| 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 | |