From a818bd461a6a6671e90dbf7ab97679b765100949 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 11 Jun 2026 12:53:29 -0400 Subject: [PATCH 1/3] Add pre-commit configuration and update dependencies in pyproject.toml --- .pre-commit-config.yaml | 41 +++++++++++++++++++ CONTRIBUTING.md | 45 ++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 91 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..976f4dbae --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +repos: + # Black - Python code formatter + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + language_version: python3.12 + files: '\.py$' + exclude: '^(data/|obsolete/|src/ssvc/utils/namespace_patterns\.py)' + + # Markdownlint - Markdown linting with auto-fix + - repo: local + hooks: + - id: markdownlint-fix + name: markdownlint + entry: markdownlint --config .markdownlint.yml --fix + language: system + files: '\.md$' + exclude: '^(data/|obsolete/)' + + # Custom hook: doctools.py - Regenerate JSON from Python decision tables + - repo: local + hooks: + - id: doctools-regenerate + name: doctools regenerate + entry: bash -c 'PYTHONPATH=src:$PYTHONPATH uv run python src/ssvc/doctools.py --datadir=./data --overwrite' + language: system + files: '^src/ssvc/(decision_points|decision_tables)/.*\.py$' + pass_filenames: false + + # Custom hook: pytest - Run tests (non-blocking) + - repo: local + hooks: + - id: pytest-check + name: pytest + entry: uv run pytest -v + language: system + types: [python] + pass_filenames: false + always_run: true + fail_fast: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2d15805b..499078c59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,51 @@ To account for different stakeholder perspectives, we benefit from a diverse gro Please see our project documentation in the [wiki](https://github.com/CERTCC/SSVC/wiki) that accompanies this repository for more information on how you can contribute to the project. +## Development Setup + +### Prerequisites + +- Python 3.12+ +- [uv](https://docs.astral.sh/uv/) - Fast Python package installer and resolver +- [pre-commit](https://pre-commit.com/) - Git hooks framework + +### Installing Dependencies and Git Hooks + +1. **Set up the development environment:** + + ```bash + make dev + ``` + +2. **Install pre-commit hooks:** + + ```bash + uv run pre-commit install + ``` + +### Pre-Commit Hooks + +This repository uses [pre-commit](https://pre-commit.com/) to enforce code quality standards before commits. The following hooks are configured: + +| Hook | Scope | Behavior | +|------|-------|----------| +| **Black** | Python files | Auto-formats code; blocks commit if changes made | +| **Markdownlint** | Markdown files | Auto-fixes linting issues; blocks commit if changes made | +| **Doctools** | Decision point/table Python files | Regenerates JSON files; blocks commit if changes made (review before staging) | +| **Pytest** | All tests | Runs full test suite; non-blocking (warning only) | + +**Running hooks manually:** + +```bash +uv run pre-commit run --all-files +``` + +**Skipping hooks (use sparingly):** + +```bash +git commit --no-verify +``` + ## Licenses See [LICENSE](https://github.com/CERTCC/SSVC/blob/main/LICENSE) diff --git a/pyproject.toml b/pyproject.toml index f51044610..ac232b2bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ "pydantic>=2.11.7", "semver>=3.0.4", "fastapi[all,standard]>=0.116.1", + "pre-commit>=4.6.0", ] dynamic = ["version",] diff --git a/uv.lock b/uv.lock index 9face400d..44c9a9e25 100644 --- a/uv.lock +++ b/uv.lock @@ -185,6 +185,7 @@ dependencies = [ { name = "mkdocstrings-python" }, { name = "networkx" }, { name = "pandas" }, + { name = "pre-commit" }, { name = "pydantic" }, { name = "pymdown-extensions" }, { name = "scikit-learn" }, @@ -219,6 +220,7 @@ requires-dist = [ { name = "mkdocstrings-python", specifier = ">=1.17.0" }, { name = "networkx", specifier = ">=3.4.2" }, { name = "pandas", specifier = ">=2.3.2" }, + { name = "pre-commit", specifier = ">=4.6.0" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pymdown-extensions", specifier = ">=10.21.2" }, { name = "scikit-learn", specifier = ">=1.6.1" }, @@ -244,6 +246,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.3" @@ -307,6 +318,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distlib" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -407,6 +427,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/5d/0ee71a1d67b5d028536eb1bc7e2be4409a5a7c4e529a9f74812472076832/fastapi_cloud_cli-0.2.0-py3-none-any.whl", hash = "sha256:8dc13f95246d80e625e2789a21760494e855d887f70caae109423d00064772d1", size = 19864, upload-time = "2025-09-18T14:55:43.365Z" }, ] +[[package]] +name = "filelock" +version = "3.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f5/3557bf28e0f1943e4849154c821533706e6dea010f96fb6aa0b6949037d1/filelock-3.29.3.tar.gz", hash = "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", size = 61956, upload-time = "2026-06-10T17:37:11.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/8f/b61d427c4f49a8bdadc93f4e7e74df8a6df6f77ee6e26bf0df53d3925363/filelock-3.29.3-py3-none-any.whl", hash = "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d", size = 42324, upload-time = "2026-06-10T17:37:10.37Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -500,6 +529,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/aa/ffc16b7ed17e052e7d2140e66336f829e9383f61f31249c4d181e12fcdea/icontract-2.7.3-py3-none-any.whl", hash = "sha256:8fb0f93f71211416f214f37c01b8017fdc8c079d70159f49f0ddcc10918c14d0", size = 40983, upload-time = "2026-01-29T13:11:13.198Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -891,6 +929,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.3.3" @@ -1051,6 +1098,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "pybtex" version = "0.25.1" @@ -1257,6 +1320,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1934,6 +2010,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, ] +[[package]] +name = "virtualenv" +version = "21.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" From 8d468757325a8f79fe3097ea7a7acf39aaa6efd2 Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 11 Jun 2026 13:04:53 -0400 Subject: [PATCH 2/3] Restrict pytest hook to Python file changes only - Remove 'always_run: true' so pytest only runs when *.py files change - Add 'files' pattern to match Python files - Reduces unnecessary test runs on non-Python commits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 976f4dbae..e1e82b7c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,6 @@ repos: name: pytest entry: uv run pytest -v language: system - types: [python] + files: '\.py$' pass_filenames: false - always_run: true fail_fast: false From a1a9003c3ff30bcc0f70c3d96c42b1e2e0ee7b3e Mon Sep 17 00:00:00 2001 From: "Allen D. Householder" Date: Thu, 11 Jun 2026 13:16:41 -0400 Subject: [PATCH 3/3] Improve pre-commit config for production readiness - Use YAML anchor for Black version (easier to update in future) - Broaden doctools trigger from decision_points/tables to all src/ssvc/ Python - Exclude src/ssvc/api/ and src/ssvc/test/ from doctools (avoid false positives) - Change pytest from verbose (-v) to quiet (-q) with short tracebacks (--tb=short) - Only show test output on failures for cleaner commits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pre-commit-config.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1e82b7c3..828b8d662 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Black - Python code formatter - repo: https://github.com/psf/black - rev: 25.1.0 + rev: &black-version 25.1.0 hooks: - id: black language_version: python3.12 @@ -25,7 +25,8 @@ repos: name: doctools regenerate entry: bash -c 'PYTHONPATH=src:$PYTHONPATH uv run python src/ssvc/doctools.py --datadir=./data --overwrite' language: system - files: '^src/ssvc/(decision_points|decision_tables)/.*\.py$' + files: '^src/ssvc/.*\.py$' + exclude: '^(src/ssvc/api/|src/ssvc/test/)' pass_filenames: false # Custom hook: pytest - Run tests (non-blocking) @@ -33,7 +34,7 @@ repos: hooks: - id: pytest-check name: pytest - entry: uv run pytest -v + entry: uv run pytest --tb=short -q language: system files: '\.py$' pass_filenames: false