Add license and doc to rpm package
[pyi2ncommon] / pycheck.sh
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