From d8376367ef78956d21ce5a679a8824dc9aa1fb5d Mon Sep 17 00:00:00 2001 From: Jean-Christophe Fillion-Robin Date: Sun, 18 Aug 2024 09:30:17 -0400 Subject: [PATCH] ENH: Add CI infrastructure for pre-commit checks Introduce a noxfile with two sessions: 1. pre-commit: Runs pre-commit checks on all files except the ones generated by cookiecutter. 2. pre-commit-cookie: Runs pre-commit checks on a generated folder based on the cookiecutter template. The approach was adapted from the implementation at https://github.com/scientific-python/cookie/, originally contributed by @henryiii. Add corresponding GitHub Actions workflows in lint.yml to automate the pre-commit checks. --- .github/workflows/lint.yml | 29 +++ .gitignore | 170 ++++++++++++++++++ .pre-commit-config.yaml | 49 +++++ .ruff.toml | 53 ++++++ noxfile.py | 99 ++++++++++ .../.github/workflows/lint.yml | 24 +++ .../.pre-commit-config.yaml | 36 ++++ 7 files changed, 460 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .ruff.toml create mode 100644 noxfile.py create mode 100644 {{cookiecutter.project_name}}/.github/workflows/lint.yml create mode 100644 {{cookiecutter.project_name}}/.pre-commit-config.yaml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6d0a6b3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: CI (Lint) + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + + - uses: pre-commit/action@v3.0.1 + + pre-commit-cookie: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - run: pipx run nox -s 'pre-commit-cookie' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c1894c --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..19808b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: "^{{cookiecutter\\.project_name}}/" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.6.0" + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - id: requirements-txt-fixer + + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.18.0" + hooks: + - id: blacken-docs + additional_dependencies: [black==24.*] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.5.7" + hooks: + # Run the linter. + - id: ruff + args: ["--fix", "--show-fixes"] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/rbubley/mirrors-prettier + rev: "v3.3.3" + hooks: + - id: prettier + types_or: [yaml, json] + + - repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + - id: codespell + args: ["-Lnd", "-w"] + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: "0.29.1" + hooks: + - id: check-github-workflows diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..cd80bcd --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,53 @@ +[lint] +select = [ + "ARG", # flake8-unused-arguments + "ANN", # flake8-annotations + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", "F", "W", # flake8 + "EXE", # flake8-executable + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "NPY", # NumPy specific rules + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "RET", # flake8-return + "RUF", # Ruff-specific + "S", # flake8-bandit + "SIM", # flake8-simplify + "UP", # pyupgrade + "YTT", # flake8-2020 + "W", # Warning +] +extend-ignore = [ + "ANN101", # missing-type-self + "G004", # logging-f-string + "PIE790", # unnecessary-pass + + "D10", # undocumented-public-* + "D200", # One-line docstring should fit on one line + + # Disable linting rules conflicting with "ruff formatter" + # See https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "COM812", + "COM819", + "D206", + "D300", + "E111", + "E114", + "E117", + "ISC001", + "ISC002", + "Q000", + "Q002", + "Q003", + "W191", +] +flake8-annotations.suppress-dummy-args = true +flake8-annotations.suppress-none-returning = true +pydocstyle.convention = "pep257" diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..3bf66b8 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,99 @@ +import os +import shutil +import stat +import sys +from collections.abc import Callable +from pathlib import Path + +import nox + +nox.needs_version = ">=2024.3.2" +nox.options.sessions = ["pre-commit", "pre-commit-cookie"] +nox.options.default_venv_backend = "uv|virtualenv" + + +DIR = Path(__file__).parent.resolve() + +JOB_FILE = """\ +default_context: + project_name: {project_name} +""" + + +def _remove_readonly(func: Callable[[str], None], path: str, _: object) -> None: + os.chmod(path, stat.S_IWRITE) + func(path) + + +def rmtree_ro(path: Path) -> None: + if sys.version_info >= (3, 12): + shutil.rmtree(path, onexc=_remove_readonly) + else: + shutil.rmtree(path, onerror=_remove_readonly) + + +def make_cookie(session: nox.Session) -> None: + project_name = "Awesome" + package_dir = Path(project_name) + if package_dir.exists(): + rmtree_ro(package_dir) + + Path("input.yml").write_text( + JOB_FILE.format(project_name=project_name), encoding="utf-8" + ) + + session.run( + "cookiecutter", + "--no-input", + f"{DIR}", + "--config-file=input.yml", + ) + + init_git(session, package_dir) + + return package_dir + + +def init_git(session: nox.Session, package_dir: Path) -> None: + session.run("git", "-C", f"{package_dir}", "init", "-q", external=True) + session.run("git", "-C", f"{package_dir}", "add", ".", external=True) + session.run( + "git", + "-C", + f"{package_dir}", + "-c", + "user.name=Kitware Bot", + "-c", + "user.email=kwrobot@kitware.com", + "commit", + "-qm", + "feat: initial version", + external=True, + ) + + +@nox.session(name="pre-commit-cookie") +def pre_commit_cookie(session: nox.Session) -> None: + session.install("cookiecutter", "jinja2-github", "pre-commit") + + tmp_dir = session.create_tmp() + session.cd(tmp_dir) + cookie = make_cookie(session) + session.chdir(cookie) + + session.run( + "pre-commit", + "run", + "--all-files", + "--hook-stage=manual", + "--show-diff-on-failure", + ) + + +@nox.session(name="pre-commit") +def pre_commit(session: nox.Session) -> str: + """ + Run linters on the codebase. + """ + session.install("pre-commit") + session.run("pre-commit", "run", "-a") diff --git a/{{cookiecutter.project_name}}/.github/workflows/lint.yml b/{{cookiecutter.project_name}}/.github/workflows/lint.yml new file mode 100644 index 0000000..e4ead7c --- /dev/null +++ b/{{cookiecutter.project_name}}/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: CI (Lint) + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +concurrency: + group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} + cancel-in-progress: true + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - uses: pre-commit/action@v3.0.1 diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/{{cookiecutter.project_name}}/.pre-commit-config.yaml new file mode 100644 index 0000000..af3539e --- /dev/null +++ b/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.6.0" + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: "\\.(md5|svg|vtk|vtp)$|^Resources\\/[^\\/]+\\.h$|Data\\/Input\\/.+$" + - id: mixed-line-ending + exclude: "\\.(svg|vtk|vtp)$" + - id: trailing-whitespace + exclude: "\\.(svg|vtk|vtp)$" + - id: requirements-txt-fixer + + - repo: https://github.com/rbubley/mirrors-prettier + rev: "v3.3.3" + hooks: + - id: prettier + types_or: [yaml, json] + + - repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + - id: codespell + exclude: "(.png|.svg)$" + args: ["-L", "dependees,normaly,therefrom"] + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: "0.29.1" + hooks: + - id: check-github-workflows