Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/format-and-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/isort.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 55 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -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==10.0.1.1"]
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
6 changes: 3 additions & 3 deletions azure/azure_style.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion azure/clang-format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion azure/fprettify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion generate-pylintrc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
56 changes: 56 additions & 0 deletions pre-commit/README.md
Original file line number Diff line number Diff line change
@@ -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: <tag-or-sha> # 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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
File renamed without changes.
156 changes: 156 additions & 0 deletions pre-commit/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""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
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:]


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()]))


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 = '<base>'
<local overrides>
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, *extra: 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, *extra, "--force-exclude", "--config", cfg, *_argv_files()])
finally:
if is_temp:
with contextlib.suppress(FileNotFoundError):
os.unlink(cfg)
sys.exit(rc)


def ruff_check() -> None:
_ruff("check", "--fix", "--exit-non-zero-on-fix")


def ruff_format() -> None:
_ruff("format")


def isort() -> None:
# --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.
_run_with_local_config("isort", ".isort.cfg", "--settings-path", "--filter-files")


def pylint() -> None:
_run_with_local_config("pylint", ".pylintrc", "--rcfile")


def clang_format() -> None:
# clang-format < 14 has no `--style=file:<path>`; it only discovers a file
# 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.
# 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:
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:
_run_with_local_config("fprettify", ".fprettify.rc", "-c")
36 changes: 36 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[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. Deps and pinned versions come from pre-commit.
dependencies = []

[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 = ["*", ".*"]