From cda9356e87cddc83fca6957ec3d0c76f4d6860e6 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:55:19 -0700 Subject: [PATCH 01/12] move pre-commit files --- .clang-format => pre-commit/_config/.clang-format | 0 .fprettify.rc => pre-commit/_config/.fprettify.rc | 0 .isort.cfg => pre-commit/_config/.isort.cfg | 0 .pylintrc => pre-commit/_config/.pylintrc | 0 ruff.toml => pre-commit/_config/ruff.toml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename .clang-format => pre-commit/_config/.clang-format (100%) rename .fprettify.rc => pre-commit/_config/.fprettify.rc (100%) rename .isort.cfg => pre-commit/_config/.isort.cfg (100%) rename .pylintrc => pre-commit/_config/.pylintrc (100%) rename ruff.toml => pre-commit/_config/ruff.toml (100%) diff --git a/.clang-format b/pre-commit/_config/.clang-format similarity index 100% rename from .clang-format rename to pre-commit/_config/.clang-format diff --git a/.fprettify.rc b/pre-commit/_config/.fprettify.rc similarity index 100% rename from .fprettify.rc rename to pre-commit/_config/.fprettify.rc diff --git a/.isort.cfg b/pre-commit/_config/.isort.cfg similarity index 100% rename from .isort.cfg rename to pre-commit/_config/.isort.cfg diff --git a/.pylintrc b/pre-commit/_config/.pylintrc similarity index 100% rename from .pylintrc rename to pre-commit/_config/.pylintrc diff --git a/ruff.toml b/pre-commit/_config/ruff.toml similarity index 100% rename from ruff.toml rename to pre-commit/_config/ruff.toml From 8e1f67c11c25da8d2290a6cdaafbcaf6efd7b287 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:58:34 -0700 Subject: [PATCH 02/12] add files --- .pre-commit-hooks.yaml | 55 +++++++++++ pre-commit/_config/__init__.py | 0 pre-commit/cli.py | 170 +++++++++++++++++++++++++++++++++ pyproject.toml | 55 +++++++++++ 4 files changed, 280 insertions(+) create mode 100644 .pre-commit-hooks.yaml create mode 100644 pre-commit/_config/__init__.py create mode 100644 pre-commit/cli.py create mode 100644 pyproject.toml diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..733ac49 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,55 @@ +- id: mdolab-ruff-check + name: mdolab ruff (lint) + description: > + Ruff lint using mdolab's shared ruff.toml. A consuming repo's ./ruff.toml + (override keys only) is layered on top via ruff's `extend`. + entry: mdolab-ruff-check + language: python + additional_dependencies: ["ruff==0.15.16"] + types_or: [python, pyi] + require_serial: true + +- id: mdolab-ruff-format + name: mdolab ruff (format) + description: Ruff formatter using mdolab's shared ruff.toml (extendable locally). + entry: mdolab-ruff-format + language: python + additional_dependencies: ["ruff==0.15.16"] + types_or: [python, pyi] + require_serial: true + +- id: mdolab-isort + name: mdolab isort + description: isort using mdolab's shared isort.cfg. + entry: mdolab-isort + language: python + additional_dependencies: ["isort==8.0.1"] + types: [python] + require_serial: true + +- id: mdolab-pylint + name: mdolab pylint + description: pylint using mdolab's shared pylintrc. + entry: mdolab-pylint + language: python + additional_dependencies: ["pylint==4.0.6"] + types: [python] + require_serial: true + +- id: mdolab-clang-format + name: mdolab clang-format + description: clang-format using mdolab's shared style file. + entry: mdolab-clang-format + language: python + additional_dependencies: ["clang-format==22.1.5"] + types_or: [c, c++, cuda] + require_serial: true + +- id: mdolab-fprettify + name: mdolab fprettify + description: fprettify using mdolab's shared fprettify.rc. + entry: mdolab-fprettify + language: python + additional_dependencies: ["fprettify==0.3.7"] + files: '\.([fF]|[fF]9[05]|[fF]0[38])$' + require_serial: true diff --git a/pre-commit/_config/__init__.py b/pre-commit/_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pre-commit/cli.py b/pre-commit/cli.py new file mode 100644 index 0000000..61e7b71 --- /dev/null +++ b/pre-commit/cli.py @@ -0,0 +1,170 @@ +"""Console-script entry points for mdolab's repackaged pre-commit hooks. + +Each entry point: + 1. locates its bundled config, shipped as package data (importlib.resources), + 2. locates the pinned tool installed in the SAME isolated environment that + pre-commit built (beside this interpreter), and + 3. execs the tool against the files pre-commit passes as argv. + +Nothing here relies on a globally installed tool or on PATH ordering. +""" + +import contextlib +import os +import re +import shutil +import subprocess +import sys +import tempfile +from importlib.resources import as_file, files + + +@contextlib.contextmanager +def _config(name: str): + """Yield a real filesystem path to a bundled config file.""" + resource = files("_config").joinpath(name) + with as_file(resource) as path: + yield str(path) + + +def _tool(name: str) -> str: + """Absolute path to a pinned tool installed beside this interpreter. + + pre-commit's venv puts the interpreter and the tool console scripts in the + same bin/ directory, so resolving relative to sys.executable is exact and + PATH-independent. Falls back to PATH only as a courtesy. + """ + bindir = os.path.dirname(sys.executable) + for candidate in (os.path.join(bindir, name), os.path.join(bindir, name + ".exe")): + if os.path.isfile(candidate): + return candidate + found = shutil.which(name) + if found: + return found + sys.exit(f"mdolab-pre-commit: pinned tool '{name}' missing from the hook environment") + + +def _run(tool: str, args: list[str]) -> int: + return subprocess.call([_tool(tool), *args]) + + +def _argv_files() -> list[str]: + return sys.argv[1:] + + +# --------------------------------------------------------------------------- # +# ruff: shared base config, optionally extended by a local ./ruff.toml +# --------------------------------------------------------------------------- # + + +def _resolve_ruff_config(global_cfg: str) -> tuple[str, bool]: + """Return (config_path, is_temp). + + - No local ./ruff.toml -> use the shared base directly. + - Local ./ruff.toml present -> synthesize a temp config: + extend = '' + + so the shared base is inherited and the local file layers on top. Ruff + does not merge configs implicitly; `extend` is the supported mechanism, + and an absolute path sidesteps the unknown pre-commit cache location. + - Local file already declares its own top-level `extend` -> respect it. + """ + local = os.path.join(os.getcwd(), "ruff.toml") # pre-commit cwd == repo root + if not os.path.isfile(local): + return global_cfg, False + + with open(local, encoding="utf-8") as fh: + local_text = fh.read() + + if re.search(r"(?m)^\s*extend\s*=", local_text): + return local, False + + fd, tmp = tempfile.mkstemp(prefix="mdolab-ruff.", suffix=".toml") + with os.fdopen(fd, "w", encoding="utf-8") as out: + # single-quoted TOML literal string => no escaping, safe on Windows paths + out.write(f"extend = '{global_cfg}'\n") + out.write(local_text) + return tmp, True + + +def _ruff(subcommand: str) -> None: + with _config("ruff.toml") as global_cfg: + cfg, is_temp = _resolve_ruff_config(global_cfg) + try: + # --force-exclude: pre-commit always passes an explicit file list, + # and ruff checks explicitly-named files even when they match the + # config's exclude/extend-exclude unless this flag is set. Without + # it, a consuming repo's excludes are silently ignored on commit. + rc = _run("ruff", [subcommand, "--force-exclude", "--config", cfg, *_argv_files()]) + finally: + if is_temp: + with contextlib.suppress(FileNotFoundError): + os.unlink(cfg) + sys.exit(rc) + + +def ruff_check() -> None: + # add "--fix" to the arg list here if you want autofix-on-commit + _ruff("check") + + +def ruff_format() -> None: + _ruff("format") + + +# --------------------------------------------------------------------------- # +# the rest: fixed bundled config, no merging +# --------------------------------------------------------------------------- # + + +def _has_local_isort_config() -> bool: + """True if the consuming repo defines its own isort configuration. + + Mirrors isort's own discovery (and the CI style job): a dedicated + .isort.cfg, or an [isort] section in setup.cfg / tox.ini, or a + [tool.isort] table in pyproject.toml. pre-commit runs hooks from the + repo root, so we look there. + """ + root = os.getcwd() + if os.path.isfile(os.path.join(root, ".isort.cfg")): + return True + for fname, section in ( + ("setup.cfg", "[isort]"), + ("tox.ini", "[isort]"), + ("pyproject.toml", "[tool.isort]"), + ): + path = os.path.join(root, fname) + if os.path.isfile(path): + with open(path, encoding="utf-8") as fh: + if section in fh.read(): + return True + return False + + +def isort() -> None: + # Prefer the consuming repo's own isort config when present (matching the + # CI style job); otherwise impose the shared bundled config. + # --filter-files: isort's skip / skip_glob only apply during directory + # recursion; pre-commit passes an explicit file list, so without this flag + # skipped files (e.g. __init__.py) would still be sorted. Mirrors ruff's + # --force-exclude. + if _has_local_isort_config(): + sys.exit(_run("isort", ["--filter-files", *_argv_files()])) + with _config(".isort.cfg") as cfg: + sys.exit(_run("isort", ["--settings-path", cfg, "--filter-files", *_argv_files()])) + + +def pylint() -> None: + with _config(".pylintrc") as cfg: + sys.exit(_run("pylint", ["--rcfile", cfg, *_argv_files()])) + + +def clang_format() -> None: + # --style=file: accepts an arbitrary path (need not be named .clang-format) + with _config(".clang-format") as cfg: + sys.exit(_run("clang-format", [f"--style=file:{cfg}", "-i", *_argv_files()])) + + +def fprettify() -> None: + with _config(".fprettify.rc") as cfg: + sys.exit(_run("fprettify", ["-c", cfg, *_argv_files()])) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1ce4887 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "mdolab-precommit-hooks" +version = "1.0.0" +description = "mdolab's repackaged pre-commit hooks with pinned tools and shared configs." +requires-python = ">=3.9" + +# Intentionally empty. The package itself is pure-stdlib (see cli.py), so the +# base `pip install` that pre-commit runs for every hook pulls in NO tools. +# Each hook pins and installs only the tool it needs via additional_dependencies +# in .pre-commit-hooks.yaml. Enabling only the ruff hooks therefore never +# installs isort / pylint / clang-format / fprettify. +dependencies = [] + +[project.optional-dependencies] +# Local development / testing convenience only. Consumers never need this -- +# pre-commit installs the per-hook pins from .pre-commit-hooks.yaml. These pins +# are kept identical to the manifest by tests/test_pin_consistency.py. +dev = [ + "ruff==0.15.17", + "isort==8.0.1", + "pylint==4.0.6", + "clang-format==22.1.5", + "fprettify==0.3.7", + "pytest", + "pyyaml", + "tomli; python_version < '3.11'", +] + +[project.scripts] +mdolab-ruff-check = "cli:ruff_check" +mdolab-ruff-format = "cli:ruff_format" +mdolab-isort = "cli:isort" +mdolab-pylint = "cli:pylint" +mdolab-clang-format = "cli:clang_format" +mdolab-fprettify = "cli:fprettify" + +# src-layout: importable code lives under pre-commit/, not the repo root. +# cli.py is a standalone top-level module (py-modules); _config is a tiny +# package that exists only to ship the bundled tool configs as package data. +[tool.setuptools] +package-dir = {"" = "pre-commit"} +py-modules = ["cli"] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["pre-commit"] + +# The shared configs are dotfiles, so the leading-dot glob is required -- +# a bare "*" does not match names beginning with ".". +[tool.setuptools.package-data] +_config = ["*", ".*"] From 846dc4c0f528eaf98392e99d8ca40b7be3ef0e13 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:28:57 -0700 Subject: [PATCH 03/12] Fix ruff dev pin mismatch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ce4887..032be8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [] # pre-commit installs the per-hook pins from .pre-commit-hooks.yaml. These pins # are kept identical to the manifest by tests/test_pin_consistency.py. dev = [ - "ruff==0.15.17", + "ruff==0.15.16", "isort==8.0.1", "pylint==4.0.6", "clang-format==22.1.5", From 04f0928352453ea18a9be97a97e2a3bf122bf2b4 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:33:47 -0700 Subject: [PATCH 04/12] Add --fix to ruff-check hook and repoint workflows at moved config paths --- .github/workflows/format-and-lint.yaml | 2 +- .github/workflows/isort.yaml | 2 +- .github/workflows/pylint.yaml | 2 +- azure/azure_style.yaml | 6 +++--- azure/clang-format.sh | 2 +- azure/fprettify.sh | 2 +- generate-pylintrc.sh | 2 +- pre-commit/cli.py | 10 ++++++---- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/format-and-lint.yaml b/.github/workflows/format-and-lint.yaml index dbd4733..0cc2bfa 100644 --- a/.github/workflows/format-and-lint.yaml +++ b/.github/workflows/format-and-lint.yaml @@ -17,7 +17,7 @@ jobs: # Set the ruff config in the .github repo as the global config, then the local config should extend it by including the line `extend = "~/.config/ruff/ruff.toml"` mkdir -p ~/.config/ruff - url=https://raw.githubusercontent.com/mdolab/.github/main/ruff.toml + url=https://raw.githubusercontent.com/mdolab/.github/main/pre-commit/_config/ruff.toml wget $url -O ~/.config/ruff/ruff.toml echo "Ruff config:" diff --git a/.github/workflows/isort.yaml b/.github/workflows/isort.yaml index 64d784b..08e5856 100644 --- a/.github/workflows/isort.yaml +++ b/.github/workflows/isort.yaml @@ -14,6 +14,6 @@ jobs: run: | # copy over the isort config file if [[ ! -f ".isort.cfg" ]]; then - wget https://raw.githubusercontent.com/mdolab/.github/main/.isort.cfg + wget https://raw.githubusercontent.com/mdolab/.github/main/pre-commit/_config/.isort.cfg fi - uses: isort/isort-action@master diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml index 9fb2e63..2d6e6d6 100644 --- a/.github/workflows/pylint.yaml +++ b/.github/workflows/pylint.yaml @@ -17,5 +17,5 @@ jobs: - name: Run pylint run: | # copy over the pylint config file - wget https://raw.githubusercontent.com/mdolab/.github/main/.pylintrc + wget https://raw.githubusercontent.com/mdolab/.github/main/pre-commit/_config/.pylintrc find . -type f -name "*.py" -not -path "*/doc/*" | xargs pylint diff --git a/azure/azure_style.yaml b/azure/azure_style.yaml index 210017f..238f6ad 100644 --- a/azure/azure_style.yaml +++ b/azure/azure_style.yaml @@ -43,7 +43,7 @@ jobs: # Set the ruff config in the .github repo as the global config, then the local config should extend it by including the line `extend = "~/.config/ruff/ruff.toml"` mkdir -p ~/.config/ruff - cp ../.github/ruff.toml ~/.config/ruff/ruff.toml + cp ../.github/pre-commit/_config/ruff.toml ~/.config/ruff/ruff.toml echo "Ruff config:" cat ~/.config/ruff/ruff.toml @@ -71,7 +71,7 @@ jobs: # copy over the isort config file if [[ ! -f ".isort.cfg" ]]; then - cp ../.github/.isort.cfg . + cp ../.github/pre-commit/_config/.isort.cfg . fi pip install wheel @@ -94,7 +94,7 @@ jobs: cd ${{ parameters.REPO_NAME }} # copy over the pylint config file - cp ../.github/.pylintrc . + cp ../.github/pre-commit/_config/.pylintrc . pip install pylint find . -type f -name "*.py" -not -path "*/doc/*" | xargs pylint diff --git a/azure/clang-format.sh b/azure/clang-format.sh index ac6ded6..1b868b1 100644 --- a/azure/clang-format.sh +++ b/azure/clang-format.sh @@ -24,7 +24,7 @@ done # Set some constants CLANGFORMAT_CONFIG_FILE="" LOCAL_CONFIG_FILE=".clang-format" -GLOBAL_CONFIG_FILE="../.github/.clang-format" +GLOBAL_CONFIG_FILE="../.github/pre-commit/_config/.clang-format" # Initialize variables global=0 diff --git a/azure/fprettify.sh b/azure/fprettify.sh index 91333e8..7fae4b6 100644 --- a/azure/fprettify.sh +++ b/azure/fprettify.sh @@ -2,7 +2,7 @@ FPRETTIFY_CONFIG_FILE="" LOCAL_CONFIG_FILE=".fprettify.rc" -GLOBAL_CONFIG_FILE="../.github/.fprettify.rc" +GLOBAL_CONFIG_FILE="../.github/pre-commit/_config/.fprettify.rc" if [[ ! -z "$1" ]]; then # Offer the option of supplying config file through an argument diff --git a/generate-pylintrc.sh b/generate-pylintrc.sh index e88a685..31d7221 100755 --- a/generate-pylintrc.sh +++ b/generate-pylintrc.sh @@ -5,4 +5,4 @@ pylint \ --enable basic,classes,exceptions,imports,newstyle,refactoring,stdlib,string,typecheck,variables \ --disable C,R,I,unspecified-encoding,protected-access,import-error \ --generate-rcfile \ -> .pylintrc +> pre-commit/_config/.pylintrc diff --git a/pre-commit/cli.py b/pre-commit/cli.py index 61e7b71..616ba91 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -87,7 +87,7 @@ def _resolve_ruff_config(global_cfg: str) -> tuple[str, bool]: return tmp, True -def _ruff(subcommand: str) -> None: +def _ruff(subcommand: str, *extra: str) -> None: with _config("ruff.toml") as global_cfg: cfg, is_temp = _resolve_ruff_config(global_cfg) try: @@ -95,7 +95,7 @@ def _ruff(subcommand: str) -> None: # and ruff checks explicitly-named files even when they match the # config's exclude/extend-exclude unless this flag is set. Without # it, a consuming repo's excludes are silently ignored on commit. - rc = _run("ruff", [subcommand, "--force-exclude", "--config", cfg, *_argv_files()]) + rc = _run("ruff", [subcommand, *extra, "--force-exclude", "--config", cfg, *_argv_files()]) finally: if is_temp: with contextlib.suppress(FileNotFoundError): @@ -104,8 +104,10 @@ def _ruff(subcommand: str) -> None: def ruff_check() -> None: - # add "--fix" to the arg list here if you want autofix-on-commit - _ruff("check") + # --fix / --exit-non-zero-on-fix mirror the CI style hook + # (.pre-commit-config.yaml): autofix on commit, but still fail the run when a + # fix had to be applied so nothing silently sneaks through. + _ruff("check", "--fix", "--exit-non-zero-on-fix") def ruff_format() -> None: From 8b2d14530a17888614b6379b12a93af8ec7779ff Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:20:06 -0700 Subject: [PATCH 05/12] minor cleanup --- pre-commit/cli.py | 11 +---------- pyproject.toml | 21 +-------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index 616ba91..f140eeb 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -2,8 +2,7 @@ Each entry point: 1. locates its bundled config, shipped as package data (importlib.resources), - 2. locates the pinned tool installed in the SAME isolated environment that - pre-commit built (beside this interpreter), and + 2. locates the pinned tool installed in the SAME isolated environment that pre-commit built 3. execs the tool against the files pre-commit passes as argv. Nothing here relies on a globally installed tool or on PATH ordering. @@ -104,9 +103,6 @@ def _ruff(subcommand: str, *extra: str) -> None: def ruff_check() -> None: - # --fix / --exit-non-zero-on-fix mirror the CI style hook - # (.pre-commit-config.yaml): autofix on commit, but still fail the run when a - # fix had to be applied so nothing silently sneaks through. _ruff("check", "--fix", "--exit-non-zero-on-fix") @@ -114,11 +110,6 @@ def ruff_format() -> None: _ruff("format") -# --------------------------------------------------------------------------- # -# the rest: fixed bundled config, no merging -# --------------------------------------------------------------------------- # - - def _has_local_isort_config() -> bool: """True if the consuming repo defines its own isort configuration. diff --git a/pyproject.toml b/pyproject.toml index 032be8e..1eee462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,28 +8,9 @@ version = "1.0.0" description = "mdolab's repackaged pre-commit hooks with pinned tools and shared configs." requires-python = ">=3.9" -# Intentionally empty. The package itself is pure-stdlib (see cli.py), so the -# base `pip install` that pre-commit runs for every hook pulls in NO tools. -# Each hook pins and installs only the tool it needs via additional_dependencies -# in .pre-commit-hooks.yaml. Enabling only the ruff hooks therefore never -# installs isort / pylint / clang-format / fprettify. +# Intentionally empty. Deps and pinned versions come from pre-commit. dependencies = [] -[project.optional-dependencies] -# Local development / testing convenience only. Consumers never need this -- -# pre-commit installs the per-hook pins from .pre-commit-hooks.yaml. These pins -# are kept identical to the manifest by tests/test_pin_consistency.py. -dev = [ - "ruff==0.15.16", - "isort==8.0.1", - "pylint==4.0.6", - "clang-format==22.1.5", - "fprettify==0.3.7", - "pytest", - "pyyaml", - "tomli; python_version < '3.11'", -] - [project.scripts] mdolab-ruff-check = "cli:ruff_check" mdolab-ruff-format = "cli:ruff_format" From c91f82fc7fe5d162b5ba557dc7a9c80a55a86229 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:48:49 -0700 Subject: [PATCH 06/12] Pin clang-format hook to CI version (v10) and use v10-compatible invocation with local-config preference --- .pre-commit-hooks.yaml | 2 +- pre-commit/cli.py | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 733ac49..a4e3a32 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -41,7 +41,7 @@ description: clang-format using mdolab's shared style file. entry: mdolab-clang-format language: python - additional_dependencies: ["clang-format==22.1.5"] + additional_dependencies: ["clang-format==10.0.1.1"] types_or: [c, c++, cuda] require_serial: true diff --git a/pre-commit/cli.py b/pre-commit/cli.py index f140eeb..045d0e3 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -153,9 +153,25 @@ def pylint() -> None: def clang_format() -> None: - # --style=file: accepts an arbitrary path (need not be named .clang-format) + # clang-format < 14 has no `--style=file:`; it only discovers a file + # literally named `.clang-format` by walking up from each input file. We pin + # clang-format to match the CI version (10.x), so mirror the CI script: + # - prefer the consuming repo's own .clang-format when present, otherwise + # - stage the bundled shared config at the repo root for the run and remove + # it afterward. + # pre-commit's cwd is the repo root, an ancestor of every file it passes, so + # a .clang-format there is always discovered. + staged = os.path.join(os.getcwd(), ".clang-format") # pre-commit cwd == repo root + if os.path.isfile(staged): + sys.exit(_run("clang-format", ["-style=file", "-i", *_argv_files()])) with _config(".clang-format") as cfg: - sys.exit(_run("clang-format", [f"--style=file:{cfg}", "-i", *_argv_files()])) + shutil.copyfile(cfg, staged) + try: + rc = _run("clang-format", ["-style=file", "-i", *_argv_files()]) + finally: + with contextlib.suppress(FileNotFoundError): + os.unlink(staged) + sys.exit(rc) def fprettify() -> None: From aa9bf155e7eaa4104ec07228aaf4ba593f9dc2fd Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:49:08 -0700 Subject: [PATCH 07/12] Prefer a local .fprettify.rc in the fprettify hook when present --- pre-commit/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index 045d0e3..e3c1ffb 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -175,5 +175,10 @@ def clang_format() -> None: def fprettify() -> None: + # Prefer the consuming repo's own .fprettify.rc when present (matching the CI + # script); otherwise impose the shared bundled config. + local = os.path.join(os.getcwd(), ".fprettify.rc") # pre-commit cwd == repo root + if os.path.isfile(local): + sys.exit(_run("fprettify", ["-c", local, *_argv_files()])) with _config(".fprettify.rc") as cfg: sys.exit(_run("fprettify", ["-c", cfg, *_argv_files()])) From 5baaa86bfdf7f0c2d58a4b2ee7e2465dfcc47bf1 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:07:55 -0700 Subject: [PATCH 08/12] Simplify isort hook to only honor a repo-root .isort.cfg as the local config --- pre-commit/cli.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index e3c1ffb..86f353b 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -110,39 +110,16 @@ def ruff_format() -> None: _ruff("format") -def _has_local_isort_config() -> bool: - """True if the consuming repo defines its own isort configuration. - - Mirrors isort's own discovery (and the CI style job): a dedicated - .isort.cfg, or an [isort] section in setup.cfg / tox.ini, or a - [tool.isort] table in pyproject.toml. pre-commit runs hooks from the - repo root, so we look there. - """ - root = os.getcwd() - if os.path.isfile(os.path.join(root, ".isort.cfg")): - return True - for fname, section in ( - ("setup.cfg", "[isort]"), - ("tox.ini", "[isort]"), - ("pyproject.toml", "[tool.isort]"), - ): - path = os.path.join(root, fname) - if os.path.isfile(path): - with open(path, encoding="utf-8") as fh: - if section in fh.read(): - return True - return False - - def isort() -> None: - # Prefer the consuming repo's own isort config when present (matching the - # CI style job); otherwise impose the shared bundled config. + # Prefer the consuming repo's own .isort.cfg when present (matching the CI + # style job); otherwise impose the shared bundled config. # --filter-files: isort's skip / skip_glob only apply during directory # recursion; pre-commit passes an explicit file list, so without this flag # skipped files (e.g. __init__.py) would still be sorted. Mirrors ruff's # --force-exclude. - if _has_local_isort_config(): - sys.exit(_run("isort", ["--filter-files", *_argv_files()])) + local = os.path.join(os.getcwd(), ".isort.cfg") # pre-commit cwd == repo root + if os.path.isfile(local): + sys.exit(_run("isort", ["--settings-path", local, "--filter-files", *_argv_files()])) with _config(".isort.cfg") as cfg: sys.exit(_run("isort", ["--settings-path", cfg, "--filter-files", *_argv_files()])) From e625d318c43e82a3aa9e85d78e5b02f046ead59e Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:10:25 -0700 Subject: [PATCH 09/12] Generalize local-config override into _run_with_local_config for isort and fprettify --- pre-commit/cli.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index 86f353b..fe0fb6f 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -51,6 +51,21 @@ def _argv_files() -> list[str]: return sys.argv[1:] +def _run_with_local_config(tool: str, config_name: str, flag: str, *extra: str) -> None: + """Run `tool` over pre-commit's file list, preferring a repo-local config. + + The config is passed by path via `flag` (e.g. "-c" or "--settings-path"). + If the consuming repo ships its own `config_name` at the repo root + (pre-commit's cwd), that wins; otherwise the bundled shared config is used. + `extra` args are inserted after the config and before the file list. + """ + local = os.path.join(os.getcwd(), config_name) # pre-commit cwd == repo root + if os.path.isfile(local): + sys.exit(_run(tool, [flag, local, *extra, *_argv_files()])) + with _config(config_name) as cfg: + sys.exit(_run(tool, [flag, cfg, *extra, *_argv_files()])) + + # --------------------------------------------------------------------------- # # ruff: shared base config, optionally extended by a local ./ruff.toml # --------------------------------------------------------------------------- # @@ -117,11 +132,7 @@ def isort() -> None: # recursion; pre-commit passes an explicit file list, so without this flag # skipped files (e.g. __init__.py) would still be sorted. Mirrors ruff's # --force-exclude. - local = os.path.join(os.getcwd(), ".isort.cfg") # pre-commit cwd == repo root - if os.path.isfile(local): - sys.exit(_run("isort", ["--settings-path", local, "--filter-files", *_argv_files()])) - with _config(".isort.cfg") as cfg: - sys.exit(_run("isort", ["--settings-path", cfg, "--filter-files", *_argv_files()])) + _run_with_local_config("isort", ".isort.cfg", "--settings-path", "--filter-files") def pylint() -> None: @@ -154,8 +165,4 @@ def clang_format() -> None: def fprettify() -> None: # Prefer the consuming repo's own .fprettify.rc when present (matching the CI # script); otherwise impose the shared bundled config. - local = os.path.join(os.getcwd(), ".fprettify.rc") # pre-commit cwd == repo root - if os.path.isfile(local): - sys.exit(_run("fprettify", ["-c", local, *_argv_files()])) - with _config(".fprettify.rc") as cfg: - sys.exit(_run("fprettify", ["-c", cfg, *_argv_files()])) + _run_with_local_config("fprettify", ".fprettify.rc", "-c") From 604432444f2e655dbbd79be4f4abcb2d81b41891 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:10:34 -0700 Subject: [PATCH 10/12] Honor a repo-local .pylintrc in the pylint hook --- pre-commit/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index fe0fb6f..056e543 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -136,8 +136,9 @@ def isort() -> None: def pylint() -> None: - with _config(".pylintrc") as cfg: - sys.exit(_run("pylint", ["--rcfile", cfg, *_argv_files()])) + # Prefer the consuming repo's own .pylintrc when present; otherwise impose + # the shared bundled config. + _run_with_local_config("pylint", ".pylintrc", "--rcfile") def clang_format() -> None: From b51a40799b511032d98452a9f8381004286d23db Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:16:33 -0700 Subject: [PATCH 11/12] Add README for the pre-commit hooks --- pre-commit/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 pre-commit/README.md diff --git a/pre-commit/README.md b/pre-commit/README.md new file mode 100644 index 0000000..bd3ee5c --- /dev/null +++ b/pre-commit/README.md @@ -0,0 +1,56 @@ +# mdolab pre-commit hooks + +Repackaged pre-commit hooks that run pinned tools against mdolab's shared +configs, so every consuming repo lints and formats identically without +relying on globally installed tools or PATH ordering. + +## Using the hooks + +Add to a consuming repo's `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/mdolab/.github + rev: # pin to a released tag or commit + hooks: + - id: mdolab-ruff-check + - id: mdolab-ruff-format + - id: mdolab-isort + - id: mdolab-pylint + - id: mdolab-clang-format + - id: mdolab-fprettify +``` + +Pick only the hooks you need. Then: + +```bash +pre-commit install +pre-commit run --all-files +``` + +Each hook installs its own pinned tool into an isolated environment, see [.pre-commit-hooks.yaml](../.pre-commit-hooks.yaml). + +## Local config overrides + +By default each hook uses the bundled shared config in [_config/](_config/). +A consuming repo can override by placing its own config at the repo root: + +| Hook | Local override file | Behavior | +| --------------------- | ------------------- | ------------------------------------------------ | +| `mdolab-ruff-check` | `./ruff.toml` | layered onto the shared base via ruff's `extend` | +| `mdolab-ruff-format` | `./ruff.toml` | same as above | +| `mdolab-isort` | `./.isort.cfg` | used instead of the shared config | +| `mdolab-pylint` | `./.pylintrc` | used instead of the shared config | +| `mdolab-clang-format` | `./.clang-format` | used instead of the shared config | +| `mdolab-fprettify` | `./.fprettify.rc` | used instead of the shared config | + +Ruff is special: a local `./ruff.toml` is *merged on top of* the shared base. +The other tools take the local file as-is. + +## Internals + +- [cli.py](cli.py) - the console-script entry points. Each resolves its bundled + config (`_config`, shipped as package data) and the pinned tool installed + beside `sys.executable` in pre-commit's isolated environment, then execs the + tool against the files pre-commit passes as argv. +- [_config/](_config/) - the shared configs, shipped as package data. From 43e428ac410398a11ae420e47a279e123685056d Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:18:58 -0700 Subject: [PATCH 12/12] cleanup comments --- pre-commit/cli.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/pre-commit/cli.py b/pre-commit/cli.py index 056e543..53cada0 100644 --- a/pre-commit/cli.py +++ b/pre-commit/cli.py @@ -66,11 +66,6 @@ def _run_with_local_config(tool: str, config_name: str, flag: str, *extra: str) sys.exit(_run(tool, [flag, cfg, *extra, *_argv_files()])) -# --------------------------------------------------------------------------- # -# ruff: shared base config, optionally extended by a local ./ruff.toml -# --------------------------------------------------------------------------- # - - def _resolve_ruff_config(global_cfg: str) -> tuple[str, bool]: """Return (config_path, is_temp). @@ -126,8 +121,6 @@ def ruff_format() -> None: def isort() -> None: - # Prefer the consuming repo's own .isort.cfg when present (matching the CI - # style job); otherwise impose the shared bundled config. # --filter-files: isort's skip / skip_glob only apply during directory # recursion; pre-commit passes an explicit file list, so without this flag # skipped files (e.g. __init__.py) would still be sorted. Mirrors ruff's @@ -136,18 +129,14 @@ def isort() -> None: def pylint() -> None: - # Prefer the consuming repo's own .pylintrc when present; otherwise impose - # the shared bundled config. _run_with_local_config("pylint", ".pylintrc", "--rcfile") def clang_format() -> None: # clang-format < 14 has no `--style=file:`; it only discovers a file - # literally named `.clang-format` by walking up from each input file. We pin - # clang-format to match the CI version (10.x), so mirror the CI script: + # literally named `.clang-format` by walking up from each input file: # - prefer the consuming repo's own .clang-format when present, otherwise - # - stage the bundled shared config at the repo root for the run and remove - # it afterward. + # - stage the bundled shared config at the repo root for the run and remove it afterward. # pre-commit's cwd is the repo root, an ancestor of every file it passes, so # a .clang-format there is always discovered. staged = os.path.join(os.getcwd(), ".clang-format") # pre-commit cwd == repo root @@ -164,6 +153,4 @@ def clang_format() -> None: def fprettify() -> None: - # Prefer the consuming repo's own .fprettify.rc when present (matching the CI - # script); otherwise impose the shared bundled config. _run_with_local_config("fprettify", ".fprettify.rc", "-c")