Set up pre-commit hooks

- add pre-commit hooks:
  + run `nox -s lint` on staged *.py files
  + run common pre-commit hooks for validations that could not be
    achieved with tools in the develop environment so easily
- add pre-merge hook:
  + run `nox -s _pre-commit-test-hook` before merges
    * ignores the paths to staged files
      passed in by the pre-commit framework
    * runs all test cases instead
This commit is contained in:
Alexander Hess 2024-09-10 02:32:56 +02:00
parent c07a9ed19f
commit 7a5246556a
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
5 changed files with 196 additions and 2 deletions

48
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,48 @@
default_stages:
- commit
fail_fast: true
repos:
- repo: local
hooks:
- id: local-lint
name: Lint the source files
entry: nox -s lint --
language: system
stages:
- commit
types:
- python
verbose: true
- id: local-test
name: Run the entire test suite
entry: nox -s _pre-commit-test-hook --
language: system
stages:
- merge-commit
types:
- text
verbose: true
- repo: "https://github.com/pre-commit/pre-commit-hooks"
rev: v4.6.0
hooks:
- id: check-added-large-files
args:
- "--maxkb=100"
- id: check-builtin-literals
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
stages:
- commit
- id: mixed-line-ending
args:
- "--fix=no"
- id: no-commit-to-branch
args:
- "--branch"
- main
- id: trailing-whitespace
stages:
- commit

View file

@ -95,6 +95,13 @@ The second task lints all source code files with
[ruff](https://pypi.org/project/ruff/).
`flake8` is configured with a couple of plug-ins.
You may want to install the [pre-commit](https://pre-commit.com/) hooks
that come with the project:
`nox -s pre-commit-install`
Then, the linting and testing occurs automatically before every commit.
#### Test Suite

View file

@ -250,7 +250,10 @@ def test(session: nox.Session) -> None:
install_unpinned(session, "-e", ".") # "-e" makes session reuseable
install_pinned(session, *TEST_DEPENDENCIES)
args = session.posargs or (
# If this function is run by the `pre-commit` framework, extra
# arguments are dropped by the hack inside `pre_commit_test_hook()`
posargs = () if session.env.get("_drop_posargs") else session.posargs
args = posargs or (
"--cov",
"--no-cov-on-fail",
TESTS_LOCATION,
@ -321,6 +324,39 @@ def test_docstrings(session: nox.Session) -> None:
session.run("xdoctest", "src/lalib")
@nox_session(name="pre-commit-install", python=MAIN_PYTHON, venv_backend="none")
def pre_commit_install(session: nox.Session) -> None:
"""Install `pre-commit` hooks."""
for type_ in ("pre-commit", "pre-merge-commit"):
session.run(
"poetry",
"run",
"pre-commit",
"install",
f"--hook-type={type_}",
external=True,
)
@nox_session(name="_pre-commit-test-hook", python=MAIN_PYTHON, reuse_venv=False)
def pre_commit_test_hook(session: nox.Session) -> None:
"""`pre-commit` hook to run all tests before merges.
Ignores the paths to the staged files passed in by the
`pre-commit` framework and executes all tests instead. So,
`nox -s _pre-commit-test-hook -- FILE1, ...` drops the "FILE1, ...".
"""
do_not_reuse(session)
# Little Hack: Create a flag in the env(ironment) ...
session.env["_drop_posargs"] = "true"
# ... and call `test()` directly because `session.notify()`
# creates the "test" session as a new `nox.Session` object
# that does not have the flag set
test(session)
def do_not_reuse(session: nox.Session, *, raise_error: bool = True) -> None:
"""Do not reuse a session with the "-r" flag."""
if session._runner.venv._reused: # noqa:SLF001

103
poetry.lock generated
View file

@ -149,6 +149,17 @@ files = [
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
@ -360,6 +371,17 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
[package.extras]
toml = ["tomli"]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "docstring-parser-fork"
version = "0.0.9"
@ -407,6 +429,22 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.16.0"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"},
{file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"]
typing = ["typing-extensions (>=4.12.2)"]
[[package]]
name = "flake8"
version = "7.1.1"
@ -666,6 +704,20 @@ files = [
[package.dependencies]
flake8 = "*"
[[package]]
name = "identify"
version = "2.6.0"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.8"
@ -922,6 +974,17 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
@ -1000,6 +1063,24 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pycodestyle"
version = "2.12.1"
@ -1520,6 +1601,26 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
version = "20.26.4"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
{file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[[package]]
name = "xdoctest"
version = "1.2.0"
@ -1573,4 +1674,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "cc035dcf07b2024900d20f7e2873c3c4c5497e71fa493bf04a629a087f994ec3"
content-hash = "3a34bd29eb4226a6054fe5ddba556605fcd621ceff7661334edf429d715c320f"

View file

@ -32,6 +32,8 @@ python = "^3.9"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.8"
# Code formatters
autoflake = "^2.3"
black = "^24.8"