From 7a5246556a2c9731ed2f62788cd98e48c7cbdc9f Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 10 Sep 2024 02:32:56 +0200 Subject: [PATCH] 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 --- .pre-commit-config.yaml | 48 +++++++++++++++++++ README.md | 7 +++ noxfile.py | 38 ++++++++++++++- poetry.lock | 103 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..30976b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/README.md b/README.md index a3b794e..0d2f961 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/noxfile.py b/noxfile.py index 5699fa8..1ffb18d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 06e42f9..e172dc3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index ac9b78e..99cf560 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ python = "^3.9" [tool.poetry.group.dev.dependencies] +pre-commit = "^3.8" + # Code formatters autoflake = "^2.3" black = "^24.8"