Skip to content
Draft
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
21 changes: 12 additions & 9 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ updates:
semver-major-days: 14
groups:
# Listed first, since a dependency joins the first group it matches. A new
# lint rule or type-check error in one of these must not hold up the
# runtime floor bumps grouped below, so they get a PR of their own.
# lint rule or type-check error in one of these, or a prek release that
# changes how the hooks run (prek is 0.x, where a minor release may break),
# must not hold up the runtime floor bumps grouped below, so they get a PR
# of their own.
lint-tools:
patterns: ["ruff", "mypy", "typos"]
patterns: ["ruff", "mypy", "typos", "prek"]
minor-and-patch:
update-types: ["minor", "patch"]
ignore:
Expand Down Expand Up @@ -77,12 +79,13 @@ updates:
labels:
- "dependencies"

# pre-commit hook revisions in .pre-commit-config.yaml. Dependabot follows the
# `# frozen: vX` comment on SHA-pinned revs and rewrites the SHA and the
# comment together. `repo: local` hooks (ruff, mypy, typos, uv-lock) are
# skipped: ruff, mypy and typos come from uv.lock, which the "uv" entry above
# maintains, and uv-lock runs the uv on PATH. Only `default-days` cooldown is
# supported for this ecosystem.
# Hook revisions in .pre-commit-config.yaml. The ecosystem is named after
# pre-commit, but Dependabot only reads and rewrites that file, so it works
# the same with prek. Dependabot follows the `# frozen: vX` comment on
# SHA-pinned revs and rewrites the SHA and the comment together. `repo: local`
# hooks (ruff, mypy, typos, uv-lock) are skipped: ruff, mypy and typos come
# from uv.lock, which the "uv" entry above maintains, and uv-lock runs the uv
# on PATH. Only `default-days` cooldown is supported for this ecosystem.
- package-ecosystem: "pre-commit"
directory: "/"
schedule:
Expand Down
43 changes: 26 additions & 17 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ permissions:
jobs:
# The job id is the required status check "pre-commit" on main; keep it.
#
# These steps do what pre-commit/action v3.0.1 does, written out: its last
# release pins actions/cache@v4, which targets the deprecated Node 20
# runtime, and it has had no release since. pre-commit itself comes from
# uv.lock rather than a pip install.
# prek runs the hooks in .pre-commit-config.yaml. It comes from uv.lock rather
# than j178/prek-action, so CI runs the version developers run, Dependabot's
# uv updates move it, and the dependency audit scans it.
pre-commit:
runs-on: ubuntu-24.04
env:
# Where prek keeps cloned hook repositories and hook environments. Set
# here so prek and the cache step below use the same path.
PREK_HOME: ~/.cache/prek
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -30,26 +33,32 @@ jobs:
python-version: "3.11"
enable-cache: true

# Hook environments, keyed on the config that defines them and on the
# Python they were built with, since a hook venv does not survive an
# interpreter change. This is the cache pre-commit/action used to provide.
- name: Cache pre-commit hook environments
# Hook environments, keyed on the Python they were built with, since a
# hook venv does not survive an interpreter change, on the config that
# defines them, and on uv.lock, which pins prek and the uv that builds
# them. When only uv.lock changed, the newest cache for the same config
# is restored: prek reuses the environments that still match and the
# result is saved under the new key.
- name: Cache prek hook environments
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
path: ${{ env.PREK_HOME }}
key: >-
pre-commit-${{ runner.os }}-py${{ steps.setup-uv.outputs.python-version }}-${{
hashFiles('.pre-commit-config.yaml') }}
prek-${{ runner.os }}-py${{ steps.setup-uv.outputs.python-version }}-${{
hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('uv.lock') }}
restore-keys: >-
prek-${{ runner.os }}-py${{ steps.setup-uv.outputs.python-version }}-${{
hashFiles('.pre-commit-config.yaml') }}-

# pre-commit itself comes from the locked dev group. The ruff, mypy and
# typos hooks are `repo: local` and run through `uv run --locked`, which
# syncs .venv to the default groups first: the project, its dependencies
# prek itself needs only the locked dev group. The ruff, mypy and typos
# hooks are `repo: local` and run through `uv run --locked`, which syncs
# .venv to the default groups first: the project, its dependencies
# (pydantic 2) and the dev tools, so mypy checks against the SDK's real
# dependencies.
- name: Run pre-commit
- name: Run prek
run: >-
uv run --locked
pre-commit run --all-files --show-diff-on-failure --color=always
uv run --locked --only-dev
prek run --all-files --show-diff-on-failure --color=always

# The SDK imports pydantic differently per major, so its types are checked
# against pydantic 1 as well (the hook above ran against pydantic 2).
Expand Down
43 changes: 35 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
# `language: unsupported` (the pre-commit 4.4 name for `system`) runs a command
# from the environment pre-commit was started in.
minimum_pre_commit_version: "4.4.0"
# Run by prek (see CONTRIBUTING.md). `language: system` runs a command from the
# environment prek was started in.
#
# An older prek, such as an old global install, stops with an error here rather
# than running the hooks differently.
minimum_prek_version: "0.5.3"

# The Python that `language: python` hook environments are built with: the one
# in .python-version. Without it prek builds them with whichever Python uv picks
# first, which can be newer than 3.11 or free-threaded, and check-json and
# check-toml then parse with that version's json and tomllib.
default_language_version:
python: "3.11"

repos:
# Pinned to a commit rather than a tag, which can be moved; the `# frozen:`
# comment names the release, and Dependabot updates both.
#
# `language: python` makes prek run that release's hooks, on the Python set in
# default_language_version above. Without it, prek runs its own Rust versions
# of them, which differ on some inputs: check-yaml accepts unknown tags,
# check-toml accepts TOML 1.1, check-json accepts an empty file, and others.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: trailing-whitespace
language: python
- id: end-of-file-fixer
language: python
- id: check-added-large-files
language: python
- id: check-case-conflict
language: python
- id: check-executables-have-shebangs
language: python
- id: check-shebang-scripts-are-executable
language: python
- id: check-json
language: python
- id: check-toml
language: python
- id: check-yaml
language: python
- id: check-xml
language: python
- id: check-merge-conflict
language: python
- id: mixed-line-ending
language: python
args: [--fix=lf]

# ruff, mypy and typos run from the project environment, so the versions in
Expand All @@ -32,22 +59,22 @@ repos:
- id: ruff-check
name: ruff check
entry: uv run --locked ruff check --fix
language: unsupported
language: system
# pyproject.toml too: ruff validates its [project] table (RUF200).
files: (\.pyi?|(^|/)pyproject\.toml)$
require_serial: true
- id: ruff-format
name: ruff format
entry: uv run --locked ruff format
language: unsupported
language: system
types_or: [python, pyi]
require_serial: true
- id: mypy
name: mypy
# No file names: mypy checks the `files` set in pyproject.toml as a whole,
# which is what makes cross-module errors visible.
entry: uv run --locked mypy
language: unsupported
language: system
# pyproject.toml and uv.lock too: they hold mypy's config and the
# dependency versions it checks against.
files: (\.pyi?|^pyproject\.toml|^uv\.lock)$
Expand All @@ -56,7 +83,7 @@ repos:
- id: typos
name: typos
entry: uv run --locked typos --force-exclude
language: unsupported
language: system
types: [text]
require_serial: true

Expand All @@ -70,6 +97,6 @@ repos:
- id: uv-lock
name: uv lock --check
entry: uv lock --check
language: unsupported
language: system
files: ^(uv\.lock|pyproject\.toml|uv\.toml)$
pass_filenames: false
27 changes: 19 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,29 @@ the 7-day `exclude-newer` cooldown, which produces a different `uv.lock`.

```sh
uv sync # .venv with the SDK and the dev tools, exactly as locked in uv.lock
uv run pre-commit install # lint, format, type-check and uv.lock checks on every commit
uv run prek install # lint, format, type-check and uv.lock checks on every commit
```

The hooks are defined in `.pre-commit-config.yaml` and run by [prek](https://github.com/j178/prek),
which comes from the `dev` group. If `.git/hooks/pre-commit` was installed by pre-commit, run
`uv run prek install --force` instead: without `--force`, prek keeps that hook as
`pre-commit.legacy` and runs it as well, and it fails because pre-commit is not in `.venv`.

`uv sync` installs the SDK from this checkout in editable mode, so the tests and scripts
import the working tree's `permit`. `.python-version` selects Python 3.11, the version the
end-to-end CI job runs on. The SDK itself supports Python 3.10 and later.

The ruff, mypy and typos hooks run through `uv run --locked`, which syncs `.venv` to `uv.lock`
before running the tool, so the versions in `uv.lock` are the only ones in play; the hooks fail
if `uv.lock` is out of date with `pyproject.toml`. That sync uses the default groups, so a commit
also switches a `.venv` synced with `--group pydantic-v1` back to pydantic 2.x. The same checks
by hand:
also switches a `.venv` synced with `--group pydantic-v1` back to pydantic 2.x. To run every
hook on every file, as CI does:

```sh
uv run prek run --all-files
```

The same checks one tool at a time:

```sh
uv run ruff check # lint (the rule set is `select = ["ALL"]` minus justified ignores)
Expand All @@ -47,8 +58,8 @@ uv run --group pydantic-v1 mypy
- Dev tools are exact pins in the `dev` dependency group, which `uv sync` installs by
default.
- After changing either, run `uv lock` and commit `uv.lock` with the change. The `uv-lock`
pre-commit hook fails while the two disagree, and CI installs with `uv sync --locked`,
which refuses a stale lock.
hook fails while the two disagree, and CI installs with `uv sync --locked`, which refuses
a stale lock.
- `uv lock` leaves out releases less than 7 days old (`exclude-newer` in `[tool.uv]`), but
the dependency audit does not, so it can fail on an advisory whose fix `uv lock` still
filters out. To take that fix now, add `exclude-newer-package = { <package> = false }`
Expand Down Expand Up @@ -101,8 +112,8 @@ versions the runtime requirements allow and at the newest.

`tests/test_typing_surface.py` runs mypy on `tests/type_check/consumer.py` the way a user's
project sees an installed permit, and fails while `permit/_sync_types.pyi` is out of date
(see [Regenerating the sync stubs](#regenerating-the-sync-stubs)). The `mypy` pre-commit
hook type-checks the SDK itself, strictly and with the pydantic plugin (see [Setup](#setup)).
(see [Regenerating the sync stubs](#regenerating-the-sync-stubs)). The `mypy` hook
type-checks the SDK itself, strictly and with the pydantic plugin (see [Setup](#setup)).

### The migration skill's tests

Expand Down Expand Up @@ -220,7 +231,7 @@ has to be restored by hand.
in `pyproject.toml` and keeps the generator's formatting, so the diff shows only API changes.

5. Run the schema drift check, the offline tests under both pydantic majors (see above) and
`uv run pre-commit run --all-files`.
`uv run prek run --all-files`.

### Schema drift check

Expand Down
18 changes: 9 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ Repository = "https://github.com/permitio/permit-python"
# resolve the same versions. Dependabot raises them. A pin also gives the CVE
# scan a version to evaluate: a spec with no bound has none, so a package listed
# that way is absent from every audit. These pins are the only place the ruff,
# mypy and typos versions are set: their pre-commit hooks are `repo: local` and
# run the copies `uv run --locked` installs from uv.lock, so CI lints and
# type-checks with exactly these versions, and a Dependabot bump of a pin moves
# the hook with it.
# mypy and typos versions are set: their hooks in .pre-commit-config.yaml are
# `repo: local` and run the copies `uv run --locked` installs from uv.lock, so
# CI lints and type-checks with exactly these versions, and a Dependabot bump of
# a pin moves the hook with it.
# aioresponses is left out on purpose. No test imports it, and its latest
# release (0.7.9) is incompatible with the aiohttp 3.14.3 floor: every mocked
# request raises "ClientResponse.__init__() missing 1 required keyword-only
Expand All @@ -94,7 +94,7 @@ dev = [
# Imported directly by the offline tests, which evaluate the version markers
# in [project].dependencies the way an installer does.
"packaging==26.3",
"pre-commit==4.6.2",
"prek==0.5.3",
# 9.x rather than 8.x: the old 8.3.0 floor is affected by CVE-2025-71176
# (insecure temporary directory handling), fixed in 9.0.3. Caught by this
# repo's own audit gate.
Expand All @@ -110,8 +110,8 @@ dev = [
"typos==1.50.2",
# The uv version CI runs: every setup-uv step reads it from uv.lock
# (version-file), except the publish build job, which pins its own version
# and checksum. Dependabot bumps it like any other pin. The uv-lock pre-commit
# hook runs the uv on PATH, which under `uv run` (as in CI) is this one. Run
# and checksum. Dependabot bumps it like any other pin. The uv-lock hook runs
# the uv on PATH, which under `uv run` (as in CI) is this one. Run
# this version locally so uv.lock comes out the same.
"uv==0.12.17",
# Imported directly by the offline tests (Request/Response are used to assert
Expand Down Expand Up @@ -183,8 +183,8 @@ line-length = 100
# The migration skill's sample apps are the scanner's test input, written the
# way a permit 2.x project is; their exact text is what the tests assert on.
extend-exclude = ["permit/api/models.py", "skills/tests/fixtures"]
# pre-commit passes file names explicitly, which bypasses exclusions unless
# this is set -- without it the hook would lint and reformat models.py.
# prek passes file names explicitly, which bypasses exclusions unless this
# is set -- without it the hook would lint and reformat models.py.
force-exclude = true

[tool.ruff.per-file-target-version]
Expand Down
Loading
Loading