--- /dev/null
+#!/bin/bash
+set -u
+
+# pycheck.sh
+# check a python source code file using
+# pylint, pycodestyle (former pep8) and pydocstyle
+
+# debug mode:
+#set -x
+
+readonly base_dir="$(realpath -m $(dirname $0))"
+readonly src_dir=$base_dir/src
+readonly test_dir=$base_dir/test
+
+readonly pylint_init_hook="
+import sys;
+sys.path.insert(0, 'base_dir');
+"
+readonly codestyle_ignores="E501,W503,E226,E265"
+readonly docstyle_ignores="D212,D205,D400,D401,D203,D105,D301,D302"
+
+usage_print_msg ()
+{
+ printf "usage: %s --all { FILENAME | --branch NAME }\n" "$0"
+ printf "\n"
+ printf "\twhere\n"
+ printf "\t\tFILENAME file to check (expects the basename\n"
+ printf "\t\t without suffix)\n"
+ printf "\t\t--all enable all warnings\n"
+ printf "\t\t--branch NAME a git branch to check changed files\n"
+ printf "\t\t against\n"
+ printf "\n"
+}
+
+usage () {
+ local out=1
+ case "${1:-stdout}" in
+ 2|err|stderr) out=2 ;;
+ esac
+ usage_print_msg >&"${out}";
+}
+
+# check params
+pylint_Wall=no
+lookup_mode=file
+target_name=""
+while : ; do
+ arg="${1:-}"
+ case "${arg}" in
+ "") break ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ --all)
+ echo "Enabling all pylint warnings"
+ pylint_Wall=yes
+ ;;
+ --branch)
+ shift
+ arg="${1:-}"
+ if [ -z "${arg}" ]; then
+ printf "ERROR: --branch specified but no name given\n\n" >&2
+ usage err
+ exit 1
+ fi
+ target_name="${arg}"
+ lookup_mode=branch
+ ;;
+ *)
+ if [ -z "${target_name}" ]; then
+ target_name="${arg%%.py}"
+ else
+ printf "ERROR: filename ${target_name} already specified\n" >&2
+ printf "ERROR: please specify a single file name to check\n\n" >&2
+ usage err
+ exit 1
+ fi
+ ;;
+ esac
+ shift
+done
+
+if [ -z "${target_name}" ]; then
+ printf "ERROR: no file name or branch specified\n\n" >&2
+ usage err
+ exit 1
+fi
+
+# check for presence of pylint-3
+pylint-3 --version
+if [ $? = 0 ]; then
+ pylint_bin=pylint-3
+else
+ pylint --version
+ pylint_bin=pylint
+fi
+
+function has_errors {
+ local found_errors=no
+ # append extra rules passed to the function
+ codestyle_all_ignores="$codestyle_ignores,${2:-}"
+
+ # run pylint
+ local pylint_flags=(-E)
+ if [ "${pylint_Wall}" = yes ]; then
+ pylint_flags=(--disable=logging-format-interpolation \
+ --variable-rgx='(?:[[a-z_][a-z0-9_]{2,30}$|vm)')
+ fi
+ echo "pylint:"
+ if ! ${pylint_bin} ${pylint_flags[@]} --reports=no --init-hook="$pylint_init_hook" "$1"; then
+ found_errors=yes
+ fi
+ echo
+
+ # run pycodestyle, former pep8 (or fall back to pep8 if not available)
+ style_checker="pycodestyle"
+ command -v $style_checker >/dev/null 2>&1 || style_checker=pep8
+
+ echo "pycodestyle|pep8:"
+ if ! $style_checker "$1" --ignore="$codestyle_all_ignores"; then
+ found_errors=yes
+ fi
+ echo
+
+ # run pydocstyle
+ echo "pydocstyle:"
+ if ! pydocstyle "$1" --ignore=$docstyle_ignores; then
+ found_errors=yes
+ fi
+ echo
+
+ [ "${found_errors}" = yes ]
+}
+
+declare -a found=()
+
+valid_branch ()
+{
+ local needle="${1:-master}"
+ git rev-parse --verify "${needle}" &>/dev/null
+}
+
+check_to_branch ()
+{
+ local br="${1:-master}"
+
+ if ! valid_branch ${br}; then
+ printf "ERROR: branch “%s” not known to Git\n\n" "${br}"
+ usage err
+ exit 1
+ fi
+
+ local allfiles=( $(git diff --name-only --diff-filter=d "${br}") )
+ local pyfiles=( )
+ for f in ${allfiles[@]}; do
+ if [[ "$f" =~ \.py$ ]]; then pyfiles+=( "$f" ); fi
+ done
+ if [ "${#pyfiles[@]}" -eq 0 ]; then
+ printf "ERROR: no Python files (*.py) changed between HEAD and\n"
+ printf "ERROR: asked branch “%s”\n\n" "${br}"
+ printf "ERROR: %d files changed:\n" ${#allfiles[@]}
+ for f in ${allfiles[@]}; do
+ printf "\t\t× %s\n" "$f"
+ done
+ printf "\n"
+ usage err
+ exit 1
+ fi
+
+ printf "checking %d files for tiny and understandable mistakes\n" ${#pyfiles[@]}
+ local i=0
+ for py in ${pyfiles[@]}; do
+ printf "[%d/%d] checking %s\n" $(( ++i )) ${#pyfiles[@]} "${py}"
+ if has_errors "${py}"; then
+ found+=( "${py}→BAD" )
+ else
+ found+=( "${py}→GOOD" )
+ fi
+ done
+}
+
+case "${lookup_mode}" in
+ file)
+ files=$(find $base_dir \( -path "${src_dir}/*" -name "${target_name}.py" \) \
+ -or \( -path "${test_dir}/*" -name "${target_name}.py" \) )
+
+ for f in $files; do
+ echo "Found file ${f}"
+ if has_errors "${f}"; then
+ found+=( "${f}→BAD" )
+ else
+ found+=( "${f}→GOOD" )
+ fi
+ done
+
+ # error if not found as utility either
+ if [ "${#found[@]}" -eq 0 ]; then
+ echo "Could not find ${target_name} in src/test dirs!"
+ echo
+ usage stderr
+ exit 2
+ fi
+ ;;
+ branch)
+ check_to_branch "${target_name}"
+ ;;
+ *)
+ # can’t happen
+ printf "ERROR: internal error\n"
+ printf "ERROR: please run “git blame \"%s\"” to determine who" "$0"
+ printf "ERROR: is responsible for this mess\n\n"
+ exit 1
+esac
+
+declare -r wd=$(tput cols)
+
+printf "\n"
+printf "·%.0s" $(seq 1 ${wd})
+printf "\n\n"
+printf "$0 completed sucessfully\n"
+i=0
+good=0
+for result in ${found[@]} ; do
+ (( ++i ))
+ old_IFS="${IFS}"
+ IFS=→
+ read file verdict <<<"${result}"
+ IFS="${old_IFS}"
+ if [ "${verdict}" = GOOD ]; then
+ (( ++good ))
+ fi
+ printf "\t%d : %s\t→ %s\n" "$i" "${file}" "${verdict}"
+done
+printf "\n"
+printf "summary: %d files inspected, %d tested good, %d bad\n\n" \
+ $i ${good} $(( i - good ))
+
+printf "·%.0s" $(seq 1 ${wd})
+printf "\n\n"
+
+if [ ${good} -ne $i ]; then
+ exit 1
+fi
+