Set up pre-commit hooks
- add pre-commit and pre-merge hooks: + run `poetry run nox -s pre-commit` on staged *.py files + run common pre-commit hooks for validations that could not be achieved with tools in the develop environment so easily + run `poetry run nox -s pre-merge` before merges and pushes - implement the "pre-commit" and "pre-merge" sessions in nox + include a little hack to deal with the positional arguments passed by the pre-commit framework - provide more documentation on the nox sessions
This commit is contained in:
parent
9fc5b4816a
commit
da233e2e35
4 changed files with 171 additions and 15 deletions
38
.pre-commit-config.yaml
Normal file
38
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
default_stages: [commit] # only used if a hook does not specify stages
|
||||||
|
fail_fast: true
|
||||||
|
repos:
|
||||||
|
# Run the local formatting, linting, and testing tool chains.
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: local-pre-commit-checks
|
||||||
|
name: Run code formatters and linters
|
||||||
|
entry: poetry run nox -s pre-commit --
|
||||||
|
language: system
|
||||||
|
stages: [commit]
|
||||||
|
types: [python]
|
||||||
|
- id: local-pre-merge-checks
|
||||||
|
name: Run the entire test suite
|
||||||
|
entry: poetry run nox -s pre-merge --
|
||||||
|
language: system
|
||||||
|
stages: [merge-commit, push]
|
||||||
|
types: [python]
|
||||||
|
# Enable hooks provided by the pre-commit project to
|
||||||
|
# enforce rules that local tools could not that easily.
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.2.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: [--maxkb=100]
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-builtin-literals
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
stages: [commit] # overwrite the default
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: [--fix=no]
|
||||||
|
- id: no-commit-to-branch
|
||||||
|
args: [--branch, main]
|
||||||
|
- id: trailing-whitespace
|
||||||
|
stages: [commit] # overwrite the default
|
87
noxfile.py
87
noxfile.py
|
@ -39,7 +39,11 @@ nox.options.sessions = (
|
||||||
|
|
||||||
@nox.session(name='format', python=MAIN_PYTHON)
|
@nox.session(name='format', python=MAIN_PYTHON)
|
||||||
def format_(session: Session) -> None:
|
def format_(session: Session) -> None:
|
||||||
"""Format source files with autoflake, black, and isort."""
|
"""Format source files with autoflake, black, and isort.
|
||||||
|
|
||||||
|
If no extra arguments are provided, all source files are formatted.
|
||||||
|
Otherwise, they are interpreted as paths the formatters work on recursively.
|
||||||
|
"""
|
||||||
_begin(session)
|
_begin(session)
|
||||||
_install_packages(session, 'autoflake', 'black', 'isort')
|
_install_packages(session, 'autoflake', 'black', 'isort')
|
||||||
# Interpret extra arguments as locations of source files.
|
# Interpret extra arguments as locations of source files.
|
||||||
|
@ -65,7 +69,11 @@ def format_(session: Session) -> None:
|
||||||
|
|
||||||
@nox.session(python=MAIN_PYTHON)
|
@nox.session(python=MAIN_PYTHON)
|
||||||
def lint(session: Session) -> None:
|
def lint(session: Session) -> None:
|
||||||
"""Lint source files with flake8, mypy, and pylint."""
|
"""Lint source files with flake8, mypy, and pylint.
|
||||||
|
|
||||||
|
If no extra arguments are provided, all source files are linted.
|
||||||
|
Otherwise, they are interpreted as paths the linters work on recursively.
|
||||||
|
"""
|
||||||
_begin(session)
|
_begin(session)
|
||||||
_install_packages(
|
_install_packages(
|
||||||
session,
|
session,
|
||||||
|
@ -99,20 +107,33 @@ def lint(session: Session) -> None:
|
||||||
|
|
||||||
@nox.session(python=[MAIN_PYTHON, NEXT_PYTHON])
|
@nox.session(python=[MAIN_PYTHON, NEXT_PYTHON])
|
||||||
def test(session: Session) -> None:
|
def test(session: Session) -> None:
|
||||||
"""Test the code base."""
|
"""Test the code base.
|
||||||
|
|
||||||
|
Runs the unit and integration tests (written with pytest).
|
||||||
|
|
||||||
|
If no extra arguments are provided, the entire test suite
|
||||||
|
is exexcuted and succeeds only with 100% coverage.
|
||||||
|
|
||||||
|
If extra arguments are provided, they are
|
||||||
|
forwarded to pytest without any changes.
|
||||||
|
"""
|
||||||
# Re-using an old environment is not so easy here as
|
# Re-using an old environment is not so easy here as
|
||||||
# `poetry install --no-dev` removes previously installed packages.
|
# `poetry install --no-dev` removes previously installed packages.
|
||||||
# We keep things simple and forbid such usage.
|
# We keep things simple and forbid such usage.
|
||||||
if session.virtualenv.reuse_existing:
|
if session.virtualenv.reuse_existing:
|
||||||
raise RuntimeError('The "test" session must be run with the "-r" option')
|
raise RuntimeError(
|
||||||
|
'The "test" and "pre-merge" sessions must be run without the "-r" option',
|
||||||
|
)
|
||||||
|
|
||||||
_begin(session)
|
_begin(session)
|
||||||
# Install only the non-develop dependencies
|
# Install only the non-develop dependencies and the testing tool chain.
|
||||||
# and the testing tool chain.
|
|
||||||
session.run('poetry', 'install', '--no-dev', external=True)
|
session.run('poetry', 'install', '--no-dev', external=True)
|
||||||
_install_packages(session, 'pytest', 'pytest-cov')
|
_install_packages(session, 'pytest', 'pytest-cov')
|
||||||
# Interpret extra arguments as options for pytest.
|
# Interpret extra arguments as options for pytest.
|
||||||
args = session.posargs or (
|
# They are "dropped" by the hack in the pre_merge() function
|
||||||
|
# if this function is run within the "pre-merge" session.
|
||||||
|
posargs = () if session.env.get('_drop_posargs') else session.posargs
|
||||||
|
args = posargs or (
|
||||||
'--cov',
|
'--cov',
|
||||||
'--no-cov-on-fail',
|
'--no-cov-on-fail',
|
||||||
'--cov-branch',
|
'--cov-branch',
|
||||||
|
@ -125,23 +146,54 @@ def test(session: Session) -> None:
|
||||||
|
|
||||||
@nox.session(name='pre-commit', python=MAIN_PYTHON, venv_backend='none')
|
@nox.session(name='pre-commit', python=MAIN_PYTHON, venv_backend='none')
|
||||||
def pre_commit(session: Session) -> None:
|
def pre_commit(session: Session) -> None:
|
||||||
"""Source files must be well-formed before they enter git."""
|
"""Source files must be well-formed before they enter git.
|
||||||
_begin(session)
|
|
||||||
|
Intended to be run as a pre-commit hook.
|
||||||
|
|
||||||
|
This session is a wrapper that triggers the "format" and "lint" sessions.
|
||||||
|
|
||||||
|
Passed in extra arguments are forwarded. So, if it is run as a pre-commit
|
||||||
|
hook, only the currently staged source files are formatted and linted.
|
||||||
|
"""
|
||||||
|
# "format" and "lint" are run in sessions on their own as
|
||||||
|
# session.notify() creates new Session objects.
|
||||||
session.notify('format')
|
session.notify('format')
|
||||||
session.notify('lint')
|
session.notify('lint')
|
||||||
|
|
||||||
|
|
||||||
@nox.session(name='pre-merge', python=MAIN_PYTHON, venv_backend='none')
|
@nox.session(name='pre-merge', python=MAIN_PYTHON)
|
||||||
def pre_merge(session: Session) -> None:
|
def pre_merge(session: Session) -> None:
|
||||||
"""The test suite must pass before merges are made."""
|
"""The test suite must pass before merges are made.
|
||||||
_begin(session)
|
|
||||||
session.notify('test')
|
Intended to be run either as a pre-merge or pre-push hook.
|
||||||
|
|
||||||
|
First, this session triggers the "format" and "lint" sessions via
|
||||||
|
the "pre-commit" session.
|
||||||
|
|
||||||
|
Then, it runs the "test" session ignoring any extra arguments passed in
|
||||||
|
so that the entire test suite is executed.
|
||||||
|
"""
|
||||||
|
session.notify('pre-commit')
|
||||||
|
# Little hack to not work with the extra arguments provided
|
||||||
|
# by the pre-commit framework. Create a flag in the
|
||||||
|
# env(ironment) that must contain only `str`-like objects.
|
||||||
|
session.env['_drop_posargs'] = 'true'
|
||||||
|
# Cannot use session.notify() to trigger the "test" session
|
||||||
|
# as that would create a new Session object without the flag
|
||||||
|
# in the env(ironment). Instead, run the test() function within
|
||||||
|
# the "pre-merge" session.
|
||||||
|
test(session)
|
||||||
|
|
||||||
|
|
||||||
def _begin(session: Session) -> None:
|
def _begin(session: Session) -> None:
|
||||||
"""Show generic info about a session."""
|
"""Show generic info about a session."""
|
||||||
if session.posargs:
|
if session.posargs:
|
||||||
print('extra arguments:', *session.posargs) # noqa:WPS421
|
# Part of the hack in pre_merge() to "drop" the extra arguments.
|
||||||
|
# Indicate to stdout that the passed in extra arguments are ignored.
|
||||||
|
if session.env.get('_drop_posargs') is None:
|
||||||
|
print('Provided extra argument(s):', *session.posargs) # noqa:WPS421
|
||||||
|
else:
|
||||||
|
print('The provided extra arguments are ignored') # noqa:WPS421
|
||||||
|
|
||||||
session.run('python', '--version')
|
session.run('python', '--version')
|
||||||
|
|
||||||
|
@ -159,6 +211,13 @@ def _install_packages(
|
||||||
packages respecting the pinnned versions specified in poetry's lock file.
|
packages respecting the pinnned versions specified in poetry's lock file.
|
||||||
This makes nox sessions even more deterministic.
|
This makes nox sessions even more deterministic.
|
||||||
|
|
||||||
|
IMPORTANT: This function skips installation if the current nox session
|
||||||
|
is run with the "-r" flag to re-use an existing virtual environment.
|
||||||
|
That turns nox into a fast task runner provided that a virtual
|
||||||
|
environment actually existed and does not need to be changed (e.g.,
|
||||||
|
new dependencies added in the meantime). Do not use the "-r" flag on CI
|
||||||
|
or as part of pre-commit hooks!
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session: the Session object
|
session: the Session object
|
||||||
*packages_or_pip_args: the packages to be installed or pip options
|
*packages_or_pip_args: the packages to be installed or pip options
|
||||||
|
|
60
poetry.lock
generated
60
poetry.lock
generated
|
@ -118,6 +118,14 @@ typed-ast = ">=1.4.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Validate configuration and produce human readable error messages."
|
||||||
|
name = "cfgv"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
version = "3.2.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
|
@ -447,6 +455,17 @@ version = "3.1.7"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
gitdb = ">=4.0.1,<5"
|
gitdb = ">=4.0.1,<5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "File identification library for Python"
|
||||||
|
name = "identify"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||||
|
version = "1.4.25"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
license = ["editdistance"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||||
|
@ -517,6 +536,14 @@ optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
name = "nodeenv"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "1.4.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Flexible test automation."
|
description = "Flexible test automation."
|
||||||
|
@ -584,6 +611,22 @@ version = "0.13.1"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
|
name = "pre-commit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
version = "2.6.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cfgv = ">=2.0.0"
|
||||||
|
identify = ">=1.0.0"
|
||||||
|
nodeenv = ">=0.11.1"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
toml = "*"
|
||||||
|
virtualenv = ">=20.0.8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
@ -843,7 +886,7 @@ python-versions = "*"
|
||||||
version = "1.12.1"
|
version = "1.12.1"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "1a6ddbc4c05cb41329cbac4792210897f11dda8e8ba3aa7e814a4a0d0d9c4fa8"
|
content-hash = "7a843263817a908ca01d198402d3e9310c33307ac85085f028c8fbdd7587f48f"
|
||||||
lock-version = "1.0"
|
lock-version = "1.0"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
|
|
||||||
|
@ -887,6 +930,10 @@ black = [
|
||||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||||
]
|
]
|
||||||
|
cfgv = [
|
||||||
|
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
|
||||||
|
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
|
||||||
|
]
|
||||||
click = [
|
click = [
|
||||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||||
|
@ -1032,6 +1079,10 @@ gitpython = [
|
||||||
{file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"},
|
{file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"},
|
||||||
{file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"},
|
{file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"},
|
||||||
]
|
]
|
||||||
|
identify = [
|
||||||
|
{file = "identify-1.4.25-py2.py3-none-any.whl", hash = "sha256:ccd88716b890ecbe10920659450a635d2d25de499b9a638525a48b48261d989b"},
|
||||||
|
{file = "identify-1.4.25.tar.gz", hash = "sha256:110ed090fec6bce1aabe3c72d9258a9de82207adeaa5a05cd75c635880312f9a"},
|
||||||
|
]
|
||||||
iniconfig = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"},
|
{file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"},
|
||||||
{file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"},
|
{file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"},
|
||||||
|
@ -1091,6 +1142,9 @@ mypy-extensions = [
|
||||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
]
|
]
|
||||||
|
nodeenv = [
|
||||||
|
{file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"},
|
||||||
|
]
|
||||||
nox = [
|
nox = [
|
||||||
{file = "nox-2020.5.24-py3-none-any.whl", hash = "sha256:c4509621fead99473a1401870e680b0aadadce5c88440f0532863595176d64c1"},
|
{file = "nox-2020.5.24-py3-none-any.whl", hash = "sha256:c4509621fead99473a1401870e680b0aadadce5c88440f0532863595176d64c1"},
|
||||||
{file = "nox-2020.5.24.tar.gz", hash = "sha256:61a55705736a1a73efbd18d5b262a43d55a1176546e0eb28b29064cfcffe26c0"},
|
{file = "nox-2020.5.24.tar.gz", hash = "sha256:61a55705736a1a73efbd18d5b262a43d55a1176546e0eb28b29064cfcffe26c0"},
|
||||||
|
@ -1115,6 +1169,10 @@ pluggy = [
|
||||||
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
||||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||||
]
|
]
|
||||||
|
pre-commit = [
|
||||||
|
{file = "pre_commit-2.6.0-py2.py3-none-any.whl", hash = "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"},
|
||||||
|
{file = "pre_commit-2.6.0.tar.gz", hash = "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915"},
|
||||||
|
]
|
||||||
py = [
|
py = [
|
||||||
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
||||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
||||||
|
|
|
@ -30,6 +30,7 @@ python = "^3.8"
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
# Task Runners
|
# Task Runners
|
||||||
nox = "^2020.5.24"
|
nox = "^2020.5.24"
|
||||||
|
pre-commit = "^2.6.0"
|
||||||
|
|
||||||
# Code Formatters
|
# Code Formatters
|
||||||
autoflake = "^1.3.1"
|
autoflake = "^1.3.1"
|
||||||
|
|
Loading…
Reference in a new issue