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")