Merge ~jslarraz/review-tools:add-pre-commit-hook into review-tools:master

Proposed by Jorge Sancho Larraz
Status: Merged
Merged at revision: b3c09ad0c19b8fabb9c7002d53392a8c0d412788
Proposed branch: ~jslarraz/review-tools:add-pre-commit-hook
Merge into: review-tools:master
Diff against target: 507 lines (+144/-104)
8 files modified
Makefile (+62/-26)
bin/find-unmatched-bin-versions (+10/-8)
bin/rock-check-notices (+9/-6)
bin/snap-check-notices (+30/-24)
bin/unpack-package (+1/-1)
dev/null (+0/-38)
git-hooks/pre-commit (+31/-0)
setup.py (+1/-1)
Reviewer Review Type Date Requested Status
Alex Murray Approve
Review via email: mp+465496@code.launchpad.net

Commit message

Add pre-commit hook to run black, flake8, pylint and unittests

To post a comment you must log in.
Revision history for this message
Alex Murray (alexmurray) wrote :

Thanks for this @jslarraz - I wonder if perhaps we should adapt the existing run-black etc scripts to be able to operate on a list of files, then we can make sure we run it with the same arguments etc as what is done in lpci?

Or if instead the hooks should just call `run-black` etc and run against all the files, and not worry about limiting it to the list of changed files...

Thoughts?

Revision history for this message
Jorge Sancho Larraz (jslarraz) wrote :

From my point of view, ideally we would like the git-hook to run black and linters only on modified files (as a `quick` check), and lpci to run them on the whole project anyway.

Independently on how those tools are run (i.e. git-hooks or lpci), they should be called with the same arguments. I tried to use the same arguments in the git-hook as used in current scripts (with the exception of black, as we agreed to start enforcing the format instead of just complain) but I agree that them should be handled in just one place for maintainability. Thus, scripts should be updated to be able to operate on a list of files.

I'm now wondering whether we should define those black/flake8/pylint runners directly in the Makefile defining the relevant targets and get rid of `run-black`, `run-flake8`, ` run-pylint` and `run-helper` to clean up the repository top level directory. Does it make sense?

Revision history for this message
Alex Murray (alexmurray) wrote :

Sure, I think putting them in the Makefile sounds like a good idea.

Revision history for this message
Emilia Torino (emitorino) wrote :

I only wonder if we should fix the older issues, and have the project clean? this way the check does not need to be run only on changed files.... thoughts?

Revision history for this message
Jorge Sancho Larraz (jslarraz) wrote :

@emitorino running checks only on changed files is mainly done for efficiency as you can only introduce format issues on files you are effectively modifying (hopefully). If by "older issues" you mean the code that didn't follow the black convention, it was reformatted on https://code.launchpad.net/~jslarraz/review-tools/+git/review-tools/+merge/465415, if it is anything else, please let me know to take a look :)

Revision history for this message
Jorge Sancho Larraz (jslarraz) wrote (last edit ):

Added a couple of commits on top

https://git.launchpad.net/~jslarraz/review-tools/commit/?id=dd72f359ce84c75e6502e829da1a31f65ac1c9af run black on some files that were not included or explicitly excluded in run-helper. After inspecting the output of black I don't see a good reason to don't format them properly. It will be great if you can take a look to double check I'm not missing anything there.

https://git.launchpad.net/~jslarraz/review-tools/commit/?id=52a3f0fbb8e2db4cc4926a416e18a2850dcf4942 moves test runners to makefile and update pre-commit hook accordingly. **Some special considerations as inline comments**

Revision history for this message
Emilia Torino (emitorino) wrote :

> @emitorino running checks only on changed files is mainly done for efficiency
> as you can only introduce format issues on files you are effectively modifying
> (hopefully).

ACK!

If by "older issues" you mean the code that didn't follow the
> black convention, it was reformatted on
> https://code.launchpad.net/~jslarraz/review-tools/+git/review-
> tools/+merge/465415, if it is anything else, please let me know to take a look
> :)

Ah nice! I havent run this recently, sorry!

Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM! Thanks @jslarraz

review: Approve
Revision history for this message
Alex Murray (alexmurray) wrote :

It would seem that the snap build is now failing - can you take a look @jslarraz? https://launchpad.net/~myapps-reviewers/+snap/review-tools

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index df2bc11..7acb156 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -25,6 +25,22 @@ DEB_DEPENDENCIES := \
6 SNAP_DEPENDENCIES := \
7 black
8
9+ifndef FILES
10+ FILES := \
11+ ./reviewtools/*py \
12+ ./reviewtools/tests/*py \
13+ ./bin/snap-review \
14+ ./bin/create-snap-declaration \
15+ ./bin/dump-tool \
16+ ./bin/get-base-declaration \
17+ ./bin/store-query \
18+ ./bin/rock-check-notices \
19+ ./bin/rock-updates-available \
20+ ./bin/snap-updates-available \
21+ ./bin/snap-check-* \
22+ ./bin/snap-verify-*
23+endif
24+
25 check-deb-deps:
26 @for dep in $(DEB_DEPENDENCIES); do if ! dpkg -s $$dep 1>/dev/null 2>&1; then echo "Please apt install $$dep"; exit 1; fi; done
27
28@@ -33,8 +49,40 @@ check-snap-deps:
29
30 check-deps: check-deb-deps check-snap-deps
31
32-test:
33- ./run-tests
34+black: clean
35+ black --check $(FILES)
36+
37+check-style: black
38+
39+pylint: clean
40+ PYTHONPATH=. pylint -E --disable=no-member $(FILES)
41+
42+flake8: clean
43+ flake8 $(FILES)
44+
45+check-syntax: pylint flake8
46+
47+.PHONY: check-names.list
48+check-names.list:
49+ (python3 collect-check-names-from-tests 2>&1 \
50+ | grep 'CHECK|' | cut -d '|' -f 2- | egrep -v '(skeleton|some-check)' | LC_ALL=C sort \
51+ | awk -F '|' '(l && $$1 != l1) {print l} {l1=$$1; l=$$0} END {print l}') > check-names.list
52+
53+check-names: check-names.list
54+ # make sure check-names.list is up to date
55+ cp -f check-names.list check-names.list.orig
56+ $(MAKE) check-names.list
57+ diff -Naur check-names.list.orig check-names.list || exit 1
58+ rm -f check-names.list.orig
59+
60+unittest:
61+ python3 -m unittest discover -v -s ./reviewtools/tests -p 'test_*'
62+
63+coverage:
64+ python3 -m coverage run -m unittest discover -s ./reviewtools/tests -p 'test_*'
65+
66+coverage-report:
67+ python3 -m coverage report --show-missing --omit="*skeleton*,*/dist-packages/*"
68
69 functest:
70 ./tests/test.sh
71@@ -51,27 +99,7 @@ functest-updates:
72 functest-dump-tool:
73 ./tests/test-dump-tool.sh
74
75-coverage:
76- python3 -m coverage run ./run-tests
77-
78-coverage-report:
79- python3 -m coverage report --show-missing --omit="*skeleton*,*/dist-packages/*"
80-
81-syntax-check: clean
82- ./run-flake8
83- ./run-pylint
84-
85-style-check: clean
86- ./run-black
87-
88-check-names:
89- # make sure check-names.list is up to date
90- cp -f check-names.list check-names.list.orig
91- ./collect-check-names
92- diff -Naur check-names.list.orig check-names.list || exit 1
93- rm -f check-names.list.orig
94-
95-check: check-deps test functest-updates functest-dump-tool functest syntax-check style-check check-names
96+check: check-deps check-style check-syntax check-names unittest functest-updates functest-dump-tool functest
97
98 clean:
99 rm -rf ./reviewtools/__pycache__ ./reviewtools/tests/__pycache__
100@@ -80,6 +108,14 @@ clean:
101 rm -rf ./review-tools-*
102 rm -rf ./build ./review_tools.egg-info
103
104-.PHONY: check-names.list
105-check-names.list:
106- ./collect-check-names
107+install-hooks:
108+ @if ! snap list | grep black -c >>/dev/null; then \
109+ echo '*** black snap is not installed. Please install it and run `make install-hook` again ***'; \
110+ elif ! dpkg -l | grep pylint -c >>/dev/null; then \
111+ echo '*** pylint package is not installed. Please install it and run `make install-hook` again ***'; \
112+ elif ! dpkg -l | grep flake8 -c >>/dev/null; then \
113+ echo '*** flake8 package is not installed. Please install it and run `make install-hook` again ***'; \
114+ else \
115+ echo install -m 755 -b -S .backup git-hooks/pre-commit .git/hooks/pre-commit ; \
116+ install -m 755 -b -S .backup git-hooks/pre-commit .git/hooks/pre-commit ; \
117+ fi
118diff --git a/bin/find-unmatched-bin-versions b/bin/find-unmatched-bin-versions
119index 1b85a20..4316f35 100755
120--- a/bin/find-unmatched-bin-versions
121+++ b/bin/find-unmatched-bin-versions
122@@ -22,14 +22,16 @@ for line in lines:
123 pkg = line[9:]
124 db[rel][pkg] = {}
125 elif pkg is not None and line.startswith("Version: "):
126- db[rel][pkg]['version'] = line[9:]
127- elif pkg is not None and line.startswith("Source: ") and ' (' in line:
128- db[rel][pkg]['source'] = line[8:].split(' (')[0]
129- db[rel][pkg]['source_version'] = line.split('(')[1].split(')')[0]
130- elif line == '' and pkg in db[rel]:
131- if 'version' not in db[rel][pkg] or \
132- 'source_version' not in db[rel][pkg] or \
133- db[rel][pkg]['version'] == db[rel][pkg]['source_version']:
134+ db[rel][pkg]["version"] = line[9:]
135+ elif pkg is not None and line.startswith("Source: ") and " (" in line:
136+ db[rel][pkg]["source"] = line[8:].split(" (")[0]
137+ db[rel][pkg]["source_version"] = line.split("(")[1].split(")")[0]
138+ elif line == "" and pkg in db[rel]:
139+ if (
140+ "version" not in db[rel][pkg]
141+ or "source_version" not in db[rel][pkg]
142+ or db[rel][pkg]["version"] == db[rel][pkg]["source_version"]
143+ ):
144 del db[rel][pkg]
145
146 print(json.dumps(db, sort_keys=True, indent=2))
147diff --git a/bin/rock-check-notices b/bin/rock-check-notices
148index 73406f7..e2e0271 100755
149--- a/bin/rock-check-notices
150+++ b/bin/rock-check-notices
151@@ -18,7 +18,6 @@ import argparse
152 import json
153 import os
154 import sys
155-import tarfile
156
157 import reviewtools.common as common
158 from reviewtools.common import (
159@@ -49,8 +48,10 @@ def main():
160 "--no-fetch", help="use existing security db", action="store_true"
161 )
162 parser.add_argument("--with-cves", help="show referenced cves", action="store_true")
163- parser.add_argument('--ignore-pockets',
164- help='ignore updates published to the comma-separated list of pockets')
165+ parser.add_argument(
166+ "--ignore-pockets",
167+ help="ignore updates published to the comma-separated list of pockets",
168+ )
169 (args, argv) = parser.parse_known_args()
170
171 common.REPORT_OUTPUT = "console"
172@@ -95,9 +96,11 @@ def main():
173 if "USNDB" in os.environ:
174 usndb_fn = os.environ["USNDB"]
175
176- cmd_args = ["--usn-db=%s" % usndb_fn,
177- "--rock=%s" % os.path.abspath(pkg),
178- "--ignore-pockets=%s" % args.ignore_pockets]
179+ cmd_args = [
180+ "--usn-db=%s" % usndb_fn,
181+ "--rock=%s" % os.path.abspath(pkg),
182+ "--ignore-pockets=%s" % args.ignore_pockets,
183+ ]
184 if args.with_cves:
185 cmd_args.append("--with-cves")
186
187diff --git a/bin/snap-check-notices b/bin/snap-check-notices
188index 76ef609..e1b16c2 100755
189--- a/bin/snap-check-notices
190+++ b/bin/snap-check-notices
191@@ -31,22 +31,26 @@ from reviewtools.common import (
192
193
194 def help():
195- print('''Usage:
196+ print(
197+ """Usage:
198 $ %s SNAP1 SNAP2 ...
199-''' % os.path.basename(sys.argv[0]))
200+"""
201+ % os.path.basename(sys.argv[0])
202+ )
203
204
205 def main():
206 parser = argparse.ArgumentParser(
207- prog='check-notices',
208- description='Check a snap for needed security notices'
209+ prog="check-notices", description="Check a snap for needed security notices"
210+ )
211+ parser.add_argument(
212+ "--no-fetch", help="use existing security db", action="store_true"
213+ )
214+ parser.add_argument("--with-cves", help="show referenced cves", action="store_true")
215+ parser.add_argument(
216+ "--ignore-pockets",
217+ help="ignore updates published to the comma-separated list of pockets",
218 )
219- parser.add_argument('--no-fetch', help='use existing security db',
220- action='store_true')
221- parser.add_argument('--with-cves', help='show referenced cves',
222- action='store_true')
223- parser.add_argument('--ignore-pockets',
224- help='ignore updates published to the comma-separated list of pockets')
225 (args, argv) = parser.parse_known_args()
226
227 common.REPORT_OUTPUT = "console"
228@@ -75,34 +79,36 @@ def main():
229 warn("Skipping '%s', not a regular file" % pkg)
230 continue
231
232- snap = os.path.basename(pkg).split('_')[0]
233+ snap = os.path.basename(pkg).split("_")[0]
234 rev = "unknown"
235- if '_' in pkg:
236- rev = os.path.splitext(os.path.basename(pkg))[0].split('_')[1]
237+ if "_" in pkg:
238+ rev = os.path.splitext(os.path.basename(pkg))[0].split("_")[1]
239
240 if snap in reports and rev in reports[snap]:
241 debug("Skipping %s with revision %s" % (snap, rev))
242 continue
243
244- usndb_fn = os.path.join(os.environ['SNAP_USER_COMMON'], usndb)
245- if 'USNDB' in os.environ:
246- usndb_fn = os.environ['USNDB']
247+ usndb_fn = os.path.join(os.environ["SNAP_USER_COMMON"], usndb)
248+ if "USNDB" in os.environ:
249+ usndb_fn = os.environ["USNDB"]
250
251- cmd_args = ['--usn-db=%s' % usndb_fn,
252- '--snap=%s' % os.path.abspath(pkg),
253- '--ignore-pockets=%s' % args.ignore_pockets]
254+ cmd_args = [
255+ "--usn-db=%s" % usndb_fn,
256+ "--snap=%s" % os.path.abspath(pkg),
257+ "--ignore-pockets=%s" % args.ignore_pockets,
258+ ]
259 if args.with_cves:
260- cmd_args.append('--with-cves')
261+ cmd_args.append("--with-cves")
262
263 # this carries through to available
264 if had_debug is not None:
265- os.unsetenv('SNAP_DEBUG')
266+ os.unsetenv("SNAP_DEBUG")
267 # don't include stderr in the output since we try and parse this
268 # output as JSON below and hence including errors from stderr in
269 # the output will make the JSON malformed
270 rc, out = cmd([available] + cmd_args, stderr=None)
271 if had_debug is not None:
272- os.environ['SNAP_DEBUG'] = had_debug
273+ os.environ["SNAP_DEBUG"] = had_debug
274
275 if rc != 0:
276 warn(out)
277@@ -110,7 +116,7 @@ def main():
278
279 if snap not in reports:
280 reports[snap] = dict()
281- if out == '':
282+ if out == "":
283 reports[snap][rev] = dict()
284 else:
285 reports[snap][rev] = json.loads(out)
286@@ -118,7 +124,7 @@ def main():
287 print(json.dumps(reports, indent=2, sort_keys=True))
288
289
290-if __name__ == '__main__':
291+if __name__ == "__main__":
292 try:
293 main()
294 except KeyboardInterrupt:
295diff --git a/bin/unpack-package b/bin/unpack-package
296index e85469c..4b08dbf 100755
297--- a/bin/unpack-package
298+++ b/bin/unpack-package
299@@ -5,7 +5,7 @@ import sys
300
301 from reviewtools import common
302
303-if __name__ == '__main__':
304+if __name__ == "__main__":
305 if len(sys.argv) != 3:
306 common.error("%s <pkg> <dir>" % os.path.basename(sys.argv[0]))
307
308diff --git a/collect-check-names b/collect-check-names
309deleted file mode 100755
310index a562f8e..0000000
311--- a/collect-check-names
312+++ /dev/null
313@@ -1,5 +0,0 @@
314-#!/bin/sh
315-
316-(./collect-check-names-from-tests 2>&1 | grep 'CHECK|' | cut -d '|' -f 2- \
317- | egrep -v '(skeleton|some-check)' | LC_ALL=C sort \
318- | awk -F '|' '(l && $1 != l1) {print l} {l1=$1; l=$0} END {print l}') > check-names.list
319diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit
320new file mode 100644
321index 0000000..986b90d
322--- /dev/null
323+++ b/git-hooks/pre-commit
324@@ -0,0 +1,31 @@
325+#!/usr/bin/env bash
326+
327+# If any command fails, exit immediately with that command's exit status
328+set -eo pipefail
329+
330+# Find all changed files for this commit
331+# Compute the diff only once to save a small amount of time.
332+CHANGED_FILES=$(git diff --name-only --cached --diff-filter=ACMR)
333+# Get only changed files that match our file suffix pattern
334+get_pattern_files() {
335+ pattern=$(echo "$*" | sed "s/ /\$\\\|/g")
336+ echo "$CHANGED_FILES" | { grep "$pattern$" || true; }
337+}
338+# Get all changed python files
339+PY_FILES=$(get_pattern_files .py)
340+
341+if [[ -n "$PY_FILES" ]]
342+then
343+ # Run black against changed python files for this commit
344+ make black FILES=$PY_FILES
345+ echo "black passes all altered python sources."
346+ # Run flake8 against changed python files for this commit
347+ make flake8 FILES=$PY_FILES
348+ echo "flake8 passed!"
349+ # Run pylint against changed python files for this commit
350+ make pylint FILES=$PY_FILES
351+ echo "pylint passed!"
352+fi
353+
354+make unittest
355+echo "unit tests passed!"
356diff --git a/run-black b/run-black
357deleted file mode 100755
358index 975b35e..0000000
359--- a/run-black
360+++ /dev/null
361@@ -1,8 +0,0 @@
362-#!/bin/sh
363-set -e
364-
365-if command -v black > /dev/null ; then
366- ./run-helper black
367-else
368- echo "Could not find black"
369-fi
370diff --git a/run-flake8 b/run-flake8
371deleted file mode 100755
372index 4233dd1..0000000
373--- a/run-flake8
374+++ /dev/null
375@@ -1,5 +0,0 @@
376-#!/bin/sh
377-set -e
378-
379-flake8 --version
380-./run-helper flake8
381diff --git a/run-helper b/run-helper
382deleted file mode 100755
383index e19dadd..0000000
384--- a/run-helper
385+++ /dev/null
386@@ -1,49 +0,0 @@
387-#!/bin/sh
388-set -e
389-
390-exe=
391-args=
392-env=
393-case "$1" in
394- flake8)
395- exe="$1"
396- ;;
397- black)
398- exe="$1"
399- # for now, only check
400- args="--diff --quiet"
401- ;;
402- pylint3|pylint)
403- exe="$1"
404- env="PYTHONPATH=."
405- # for now, only errors
406- args="-E --disable=no-member"
407- ;;
408- *)
409- echo "run-helper black|flake8|pylint3|pylint"
410- exit 1
411- ;;
412-esac
413-
414-echo "= $exe ="
415-if [ -n "$env" ]; then
416- export "$env"
417-fi
418-for i in ./reviewtools/*py \
419- ./reviewtools/tests/*py \
420- ./bin/snap-review \
421- ./bin/create-snap-declaration \
422- ./bin/dump-tool \
423- ./bin/get-base-declaration \
424- ./bin/store-query \
425- ./bin/rock-check-notices \
426- ./bin/rock-updates-available \
427- ./bin/snap-updates-available \
428- ./bin/snap-check-* \
429- ./bin/snap-verify-* ; do
430- if echo "$i" | grep -q 'snap-check-notices\|rock-check-notices' ; then
431- continue # shell
432- fi
433- echo "Checking $i"
434- "$exe" $args "$i"
435-done
436diff --git a/run-pylint b/run-pylint
437deleted file mode 100755
438index d00614b..0000000
439--- a/run-pylint
440+++ /dev/null
441@@ -1,10 +0,0 @@
442-#!/bin/sh
443-set -e
444-
445-if command -v pylint3 > /dev/null ; then
446- ./run-helper pylint3
447-elif command -v pylint > /dev/null ; then
448- ./run-helper pylint
449-else
450- echo "Could not find pylint"
451-fi
452diff --git a/run-tests b/run-tests
453deleted file mode 100755
454index 4c7d5a0..0000000
455--- a/run-tests
456+++ /dev/null
457@@ -1,38 +0,0 @@
458-#!/usr/bin/python3
459-'''run-tests: run the test suite'''
460-#
461-# Copyright (C) 2013 Canonical Ltd.
462-#
463-# This program is free software: you can redistribute it and/or modify
464-# it under the terms of the GNU General Public License as published by
465-# the Free Software Foundation; version 3 of the License.
466-#
467-# This program is distributed in the hope that it will be useful,
468-# but WITHOUT ANY WARRANTY; without even the implied warranty of
469-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
470-# GNU General Public License for more details.
471-#
472-# You should have received a copy of the GNU General Public License
473-# along with this program. If not, see <http://www.gnu.org/licenses/>.
474-
475-import os
476-import sys
477-import unittest
478-
479-# NOTE: changes to this file may also need to be made to
480-# 'collect-check-names-from-tests'
481-
482-test_directory = 'reviewtools/tests/'
483-if 'RT_EXTRAS_PATH' in os.environ and \
484- os.path.isdir(os.environ['RT_EXTRAS_PATH'] + '/tests'):
485- test_directory = os.environ['RT_EXTRAS_PATH'] + '/tests'
486-
487-test_filename = 'test_*'
488-if len(sys.argv) > 1:
489- test_filename = sys.argv[1]
490-
491-suite = unittest.TestLoader().discover(test_directory, pattern=test_filename)
492-test_result = unittest.TextTestRunner(verbosity=2).run(suite)
493-if len(test_result.errors) > 0 or len(test_result.failures) > 0 or \
494- len(test_result.unexpectedSuccesses) > 0:
495- sys.exit(1)
496diff --git a/setup.py b/setup.py
497index 5d16646..eb4aeb8 100755
498--- a/setup.py
499+++ b/setup.py
500@@ -10,7 +10,7 @@ import re
501 changelog = "debian/changelog"
502 if os.path.exists(changelog):
503 head = codecs.open(changelog, encoding="utf-8").readline()
504- match = re.compile(".*\((.*)\).*").match(head)
505+ match = re.compile(".*\((.*)\).*").match(head) # noqa: W605
506 if match:
507 version = match.group(1)
508

Subscribers

People subscribed via source and target branches