From 6ab514b88636482d2598037a7f74b91b1869512e Mon Sep 17 00:00:00 2001 From: oskrgab Date: Wed, 24 Dec 2025 20:12:52 -0600 Subject: [PATCH] added pre-commit hooks --- .github/workflows/release.yaml | 2 +- .gitignore | 2 +- .pre-commit-config.yaml | 54 +++++++++++++ CONTRIBUTING.md | 137 ++++++++++++++++++++++++++++++--- README.md | 2 +- pyproject.toml | 4 +- uv.lock | 88 +++++++++++++++++++++ 7 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8f759f6..b7ef74d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -202,4 +202,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index ad7fcac..8dd6ee0 100644 --- a/.gitignore +++ b/.gitignore @@ -92,4 +92,4 @@ docs/_build/ site/ .spec-workflow/ -.mcp.json \ No newline at end of file +.mcp.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bb33c65 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,54 @@ +# Pre-commit hooks for code quality +# Install: uv run pre-commit install +# Run manually: uv run pre-commit run --all-files +# Update hooks: uv run pre-commit autoupdate +# Skip hooks: git commit --no-verify + +repos: + # Standard pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + exclude: ^mkdocs\.yml$ # MkDocs uses Python-specific YAML tags + - id: check-toml + - id: check-added-large-files + args: ['--maxkb=1000'] # Prevent files >1MB + - id: check-merge-conflict + - id: check-case-conflict + - id: no-commit-to-branch + args: ['--branch=main', '--branch=dev'] # Prevent commits to protected branches + + # Ruff - Fast linting and formatting (Rust-based) + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + # Run the formatter + - id: ruff-format + types_or: [python, pyi] + + # Run the linter with auto-fix + - id: ruff + types_or: [python, pyi] + args: [--fix] + + # Ty - Fast type checking (Rust-based) + - repo: local + hooks: + - id: ty + name: ty type checker + entry: uv run ty check + language: system + types: [python] + pass_filenames: false # ty checks whole project + + # Codespell - Catch common typos + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: + - --ignore-words-list=nd,te,ue # Common false positives in scientific code + - --skip="*.ipynb,*.json,*.lock,*.svg" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 160651c..9e21d53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,11 +49,19 @@ This project is intended to be a welcoming space for collaboration. We expect al uv sync # Installs all dependencies including dev dependencies ``` -4. **Verify your setup:** +4. **Install pre-commit hooks** (IMPORTANT): + ```bash + uv run pre-commit install + ``` + + This installs git hooks that automatically check code quality before each commit and **prevent accidental commits to protected branches** (`main` and `dev`). + +5. **Verify your setup:** ```bash uv run pytest # Run tests uv run ruff check . # Check code quality uv run ty check # Type check + uv run pre-commit run --all-files # Test pre-commit hooks ``` ### Optional: Git Aliases for Easier Workflow @@ -79,6 +87,76 @@ git sync # Updates local dev git cleanup # Deletes merged feature branches ``` +### Pre-commit Hooks: Your First Line of Defense + +Pre-commit hooks run **automatically before each commit** to catch issues early. + +**What's checked:** + +1. **Branch protection** - Blocks commits to `main` and `dev` (forces feature branch workflow) +2. **Code formatting** - Auto-formats with ruff +3. **Linting** - Auto-fixes common issues with ruff +4. **Type checking** - Validates types with ty +5. **File hygiene** - Removes trailing whitespace, fixes line endings +6. **YAML/TOML validation** - Checks workflow files and pyproject.toml +7. **Spell checking** - Catches typos with codespell + +**Normal workflow:** + +```bash +git checkout -b feature/my-feature +# ... make changes ... +git add . +git commit -m "Add feature" + +# Pre-commit runs automatically: +# ✅ All checks pass → commit succeeds +# ❌ Any check fails → commit blocked, files may be auto-fixed +``` + +**If hooks auto-fix files:** + +```bash +git commit -m "Add feature" +# ruff-format............Failed +# - files were modified by this hook + +# Ruff reformatted your code, re-add and commit: +git add . +git commit -m "Add feature" +# ✅ Passes this time +``` + +**Protection from mistakes:** + +```bash +# ❌ Trying to commit on protected branch +git checkout main +git commit -m "oops" + +# Output: +# no-commit-to-branch........Failed +# You're attempting to commit on branch 'main' +# Committing directly to 'main' is not allowed. + +# ✅ Use feature branches instead +git checkout -b feature/my-fix +git commit -m "Add fix" # Works! +``` + +**Skip hooks temporarily (use sparingly!):** + +```bash +git commit --no-verify # Bypasses pre-commit hooks +# WARNING: CI will still catch issues, and you can't push to main/dev anyway! +``` + +**Update hooks to latest versions:** + +```bash +uv run pre-commit autoupdate +``` + ## Development Workflow ### Branch Strategy @@ -133,6 +211,17 @@ Use these prefixes for your branches: - Commit frequently (commit message quality doesn't matter - we squash merge) 3. **Run quality checks locally:** + + **Option A: Let pre-commit handle it (recommended)** + ```bash + git add . + git commit -m "Add feature" + # Pre-commit automatically runs: ruff format, ruff check, ty check + # You still need to run tests manually: + uv run pytest --cov=relperm + ``` + + **Option B: Run checks manually before committing** ```bash uv run ruff format . # Auto-format code uv run ruff check --fix . # Fix linting issues @@ -140,6 +229,8 @@ Use these prefixes for your branches: uv run pytest --cov=relperm # Run tests with coverage ``` + **Note:** Tests are NOT run by pre-commit hooks (too slow). Always run tests manually before pushing. + 4. **Push and create PR to `dev`:** ```bash git push -u origin feature/your-feature-name @@ -187,7 +278,9 @@ Use these prefixes for your branches: ### Recovery: Accidentally Committed to Local Main/Dev -If you commit to local `main` or `dev` by mistake: +**Note:** Pre-commit hooks should prevent this! If you're seeing this scenario, make sure you installed pre-commit hooks with `uv run pre-commit install`. + +If you somehow bypassed pre-commit hooks (or didn't install them) and committed to `main` or `dev`: ```bash # 1. Check what commits you made @@ -442,19 +535,36 @@ Documentation is automatically deployed to GitHub Pages on release. ### Before Creating a PR -1. **Run all quality checks locally:** +1. **Ensure pre-commit hooks are installed:** + ```bash + uv run pre-commit install # If you haven't already + ``` + +2. **Commit your changes** (pre-commit hooks run automatically): + ```bash + git add . + git commit -m "Clear description of changes" + # Pre-commit runs: format, lint, type check, branch protection + ``` + +3. **Run tests manually** (not covered by pre-commit): ```bash - uv run ruff format . - uv run ruff check --fix . - uv run ty check uv run pytest --cov=relperm ``` -2. **Ensure all tests pass** and **coverage is >90%** +4. **Ensure all tests pass** and **coverage is >90%** -3. **Update documentation** if you changed the API +5. **Update documentation** if you changed the API -4. **Add examples** to docstrings for new functions +6. **Add examples** to docstrings for new functions + +**Alternative:** Run all checks manually before committing: +```bash +uv run ruff format . +uv run ruff check --fix . +uv run ty check +uv run pytest --cov=relperm +``` ### Creating a PR @@ -477,6 +587,9 @@ Documentation is automatically deployed to GitHub Pages on release. ### PR Review Process - **Automated checks** will run: `test`, `lint`, `validate-source-branch` + - If you used pre-commit hooks, `lint` should already pass! + - `test` checks ensure coverage is >90% + - `validate-source-branch` confirms you're targeting the correct branch - **All checks must pass** before merging - **Maintainers will review** code quality, tests, and documentation - **Address feedback** by pushing new commits to your branch @@ -521,6 +634,12 @@ uv run ruff check --fix . # Lint and auto-fix uv run ty check # Type check uv run mkdocs serve # Preview docs locally +# Pre-commit hooks +uv run pre-commit install # Install git hooks (one-time setup) +uv run pre-commit run --all-files # Run all hooks on all files +uv run pre-commit autoupdate # Update hooks to latest versions +git commit --no-verify # Skip pre-commit hooks (emergency only) + # Git workflow git checkout dev # Switch to dev git pull origin dev # Sync with remote diff --git a/README.md b/README.md index 49c69d0..6d40f13 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,4 @@ pip install relperm ```python import relperm # Your usage examples here -``` \ No newline at end of file +``` diff --git a/pyproject.toml b/pyproject.toml index 67a3a77..0732354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,12 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "codespell>=2.4.1", "mkdocs>=1.6.1", "mkdocs-material>=9.7.1", "mkdocstrings>=1.0.0", "mkdocstrings-python>=2.0.1", + "pre-commit>=4.5.1", "pymdown-extensions>=10.19.1", "pytest>=8.4.2", "ruff>=0.14.10", @@ -67,4 +69,4 @@ python-version = "3.12" [tool.ty.rules] # "check" usually implies running all default rules, but you can be specific: # For scientific code, we generally want strict typing on mathematical functions. -# If you find 'ty' too noisy, you can downgrade specific rules to "warn" \ No newline at end of file +# If you find 'ty' too noisy, you can downgrade specific rules to "warn" diff --git a/uv.lock b/uv.lock index 974e03d..5606859 100644 --- a/uv.lock +++ b/uv.lock @@ -34,6 +34,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] +[[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.4" @@ -103,6 +112,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "codespell" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -112,6 +130,24 @@ 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.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -136,6 +172,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, ] +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -361,6 +406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, ] +[[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.2" @@ -469,6 +523,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.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -587,10 +657,12 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "codespell" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, + { name = "pre-commit" }, { name = "pymdown-extensions" }, { name = "pytest" }, { name = "ruff" }, @@ -602,10 +674,12 @@ requires-dist = [{ name = "numpy", specifier = ">=2.3.2" }] [package.metadata.requires-dev] dev = [ + { name = "codespell", specifier = ">=2.4.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-material", specifier = ">=9.7.1" }, { name = "mkdocstrings", specifier = ">=1.0.0" }, { name = "mkdocstrings-python", specifier = ">=2.0.1" }, + { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pymdown-extensions", specifier = ">=10.19.1" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.14.10" }, @@ -696,6 +770,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + [[package]] name = "watchdog" version = "6.0.0"