Commit | Line | Data |
---|---|---|
1ab5214e CH |
1 | #!/bin/bash |
2 | set -u | |
3 | ||
4 | # pycheck.sh | |
5 | # check a python source code file using | |
6 | # pylint, pycodestyle (former pep8) and pydocstyle | |
7 | ||
8 | # debug mode: | |
9 | #set -x | |
10 | ||
11 | readonly base_dir="$(realpath -m $(dirname $0))" | |
12 | readonly src_dir=$base_dir/src | |
13 | readonly test_dir=$base_dir/test | |
14 | ||
15 | readonly pylint_init_hook=" | |
16 | import sys; | |
17 | sys.path.insert(0, 'base_dir'); | |
18 | " | |
19 | readonly codestyle_ignores="E501,W503,E226,E265" | |
20 | readonly docstyle_ignores="D212,D205,D400,D401,D203,D105,D301,D302" | |
21 | ||
22 | usage_print_msg () | |
23 | { | |
24 | printf "usage: %s --all { FILENAME | --branch NAME }\n" "$0" | |
25 | printf "\n" | |
26 | printf "\twhere\n" | |
27 | printf "\t\tFILENAME file to check (expects the basename\n" | |
28 | printf "\t\t without suffix)\n" | |
29 | printf "\t\t--all enable all warnings\n" | |
30 | printf "\t\t--branch NAME a git branch to check changed files\n" | |
31 | printf "\t\t against\n" | |
32 | printf "\n" | |
33 | } | |
34 | ||
35 | usage () { | |
36 | local out=1 | |
37 | case "${1:-stdout}" in | |
38 | 2|err|stderr) out=2 ;; | |
39 | esac | |
40 | usage_print_msg >&"${out}"; | |
41 | } | |
42 | ||
43 | # check params | |
44 | pylint_Wall=no | |
45 | lookup_mode=file | |
46 | target_name="" | |
47 | while : ; do | |
48 | arg="${1:-}" | |
49 | case "${arg}" in | |
50 | "") break ;; | |
51 | --help) | |
52 | usage | |
53 | exit 0 | |
54 | ;; | |
55 | --all) | |
56 | echo "Enabling all pylint warnings" | |
57 | pylint_Wall=yes | |
58 | ;; | |
59 | --branch) | |
60 | shift | |
61 | arg="${1:-}" | |
62 | if [ -z "${arg}" ]; then | |
63 | printf "ERROR: --branch specified but no name given\n\n" >&2 | |
64 | usage err | |
65 | exit 1 | |
66 | fi | |
67 | target_name="${arg}" | |
68 | lookup_mode=branch | |
69 | ;; | |
70 | *) | |
71 | if [ -z "${target_name}" ]; then | |
72 | target_name="${arg%%.py}" | |
73 | else | |
74 | printf "ERROR: filename ${target_name} already specified\n" >&2 | |
75 | printf "ERROR: please specify a single file name to check\n\n" >&2 | |
76 | usage err | |
77 | exit 1 | |
78 | fi | |
79 | ;; | |
80 | esac | |
81 | shift | |
82 | done | |
83 | ||
84 | if [ -z "${target_name}" ]; then | |
85 | printf "ERROR: no file name or branch specified\n\n" >&2 | |
86 | usage err | |
87 | exit 1 | |
88 | fi | |
89 | ||
90 | # check for presence of pylint-3 | |
91 | pylint-3 --version | |
92 | if [ $? = 0 ]; then | |
93 | pylint_bin=pylint-3 | |
94 | else | |
95 | pylint --version | |
96 | pylint_bin=pylint | |
97 | fi | |
98 | ||
99 | function has_errors { | |
100 | local found_errors=no | |
101 | # append extra rules passed to the function | |
102 | codestyle_all_ignores="$codestyle_ignores,${2:-}" | |
103 | ||
104 | # run pylint | |
105 | local pylint_flags=(-E) | |
106 | if [ "${pylint_Wall}" = yes ]; then | |
107 | pylint_flags=(--disable=logging-format-interpolation \ | |
108 | --variable-rgx='(?:[[a-z_][a-z0-9_]{2,30}$|vm)') | |
109 | fi | |
110 | echo "pylint:" | |
111 | if ! ${pylint_bin} ${pylint_flags[@]} --reports=no --init-hook="$pylint_init_hook" "$1"; then | |
112 | found_errors=yes | |
113 | fi | |
114 | echo | |
115 | ||
116 | # run pycodestyle, former pep8 (or fall back to pep8 if not available) | |
117 | style_checker="pycodestyle" | |
118 | command -v $style_checker >/dev/null 2>&1 || style_checker=pep8 | |
119 | ||
120 | echo "pycodestyle|pep8:" | |
121 | if ! $style_checker "$1" --ignore="$codestyle_all_ignores"; then | |
122 | found_errors=yes | |
123 | fi | |
124 | echo | |
125 | ||
126 | # run pydocstyle | |
127 | echo "pydocstyle:" | |
128 | if ! pydocstyle "$1" --ignore=$docstyle_ignores; then | |
129 | found_errors=yes | |
130 | fi | |
131 | echo | |
132 | ||
133 | [ "${found_errors}" = yes ] | |
134 | } | |
135 | ||
136 | declare -a found=() | |
137 | ||
138 | valid_branch () | |
139 | { | |
140 | local needle="${1:-master}" | |
141 | git rev-parse --verify "${needle}" &>/dev/null | |
142 | } | |
143 | ||
144 | check_to_branch () | |
145 | { | |
146 | local br="${1:-master}" | |
147 | ||
148 | if ! valid_branch ${br}; then | |
149 | printf "ERROR: branch “%s” not known to Git\n\n" "${br}" | |
150 | usage err | |
151 | exit 1 | |
152 | fi | |
153 | ||
154 | local allfiles=( $(git diff --name-only --diff-filter=d "${br}") ) | |
155 | local pyfiles=( ) | |
156 | for f in ${allfiles[@]}; do | |
157 | if [[ "$f" =~ \.py$ ]]; then pyfiles+=( "$f" ); fi | |
158 | done | |
159 | if [ "${#pyfiles[@]}" -eq 0 ]; then | |
160 | printf "ERROR: no Python files (*.py) changed between HEAD and\n" | |
161 | printf "ERROR: asked branch “%s”\n\n" "${br}" | |
162 | printf "ERROR: %d files changed:\n" ${#allfiles[@]} | |
163 | for f in ${allfiles[@]}; do | |
164 | printf "\t\t× %s\n" "$f" | |
165 | done | |
166 | printf "\n" | |
167 | usage err | |
168 | exit 1 | |
169 | fi | |
170 | ||
171 | printf "checking %d files for tiny and understandable mistakes\n" ${#pyfiles[@]} | |
172 | local i=0 | |
173 | for py in ${pyfiles[@]}; do | |
174 | printf "[%d/%d] checking %s\n" $(( ++i )) ${#pyfiles[@]} "${py}" | |
175 | if has_errors "${py}"; then | |
176 | found+=( "${py}→BAD" ) | |
177 | else | |
178 | found+=( "${py}→GOOD" ) | |
179 | fi | |
180 | done | |
181 | } | |
182 | ||
183 | case "${lookup_mode}" in | |
184 | file) | |
185 | files=$(find $base_dir \( -path "${src_dir}/*" -name "${target_name}.py" \) \ | |
186 | -or \( -path "${test_dir}/*" -name "${target_name}.py" \) ) | |
187 | ||
188 | for f in $files; do | |
189 | echo "Found file ${f}" | |
190 | if has_errors "${f}"; then | |
191 | found+=( "${f}→BAD" ) | |
192 | else | |
193 | found+=( "${f}→GOOD" ) | |
194 | fi | |
195 | done | |
196 | ||
197 | # error if not found as utility either | |
198 | if [ "${#found[@]}" -eq 0 ]; then | |
199 | echo "Could not find ${target_name} in src/test dirs!" | |
200 | echo | |
201 | usage stderr | |
202 | exit 2 | |
203 | fi | |
204 | ;; | |
205 | branch) | |
206 | check_to_branch "${target_name}" | |
207 | ;; | |
208 | *) | |
209 | # can’t happen | |
210 | printf "ERROR: internal error\n" | |
211 | printf "ERROR: please run “git blame \"%s\"” to determine who" "$0" | |
212 | printf "ERROR: is responsible for this mess\n\n" | |
213 | exit 1 | |
214 | esac | |
215 | ||
216 | declare -r wd=$(tput cols) | |
217 | ||
218 | printf "\n" | |
219 | printf "·%.0s" $(seq 1 ${wd}) | |
220 | printf "\n\n" | |
221 | printf "$0 completed sucessfully\n" | |
222 | i=0 | |
223 | good=0 | |
224 | for result in ${found[@]} ; do | |
225 | (( ++i )) | |
226 | old_IFS="${IFS}" | |
227 | IFS=→ | |
228 | read file verdict <<<"${result}" | |
229 | IFS="${old_IFS}" | |
230 | if [ "${verdict}" = GOOD ]; then | |
231 | (( ++good )) | |
232 | fi | |
233 | printf "\t%d : %s\t→ %s\n" "$i" "${file}" "${verdict}" | |
234 | done | |
235 | printf "\n" | |
236 | printf "summary: %d files inspected, %d tested good, %d bad\n\n" \ | |
237 | $i ${good} $(( i - good )) | |
238 | ||
239 | printf "·%.0s" $(seq 1 ${wd}) | |
240 | printf "\n\n" | |
241 | ||
242 | if [ ${good} -ne $i ]; then | |
243 | exit 1 | |
244 | fi | |
245 |