Skip to content

chore!: modernise tooling, raise the python floor to 3.12 #minor - #9

Merged
nmichlo merged 6 commits into
mainfrom
chore/standardise-tooling
Sep 9, 2026
Merged

nmichlo merged 6 commits into
mainfrom
chore/standardise-tooling

Conversation

@nmichlo

@nmichlo nmichlo commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Part of standardising CI and tooling across my repos against
nmichlo/.github.

The release chain could never publish

version-bump.yml tagged on merge; python-publish.yml triggered on tag push. A tag
pushed with GITHUB_TOKEN does not trigger another workflow, so the publish never fired.
Both are replaced by a caller of the shared release.yaml, which owns both triggers in one
run. lint.yaml and test.yaml call the shared workflows too.

Python floor 3.8 -> 3.12

Breaking for downstream consumers. Classifiers and CI matrices updated to match. The
code already used from enum import StrEnum, which needs 3.11 -- so the declared floor was
wrong, not just old.

A real bug in relative-import parsing

_core/module_imports_ast.py crashed on a bare relative import:

from . import x     # -> TypeError: sequence item 1: expected str instance, NoneType found

ast.ImportFrom.module is None for from . import x (it is only set for
from .sub import x), but the code appended it unconditionally before ".".join(...).
Found by ty's invalid-argument-type check on list.append. Fixed, with
tests/test_module_data.py::test_get_module_imports_bare_relative as a regression test.

Given this repo's whole job is parsing imports, that is a good one to have caught.

The self-hook was running 3.12 code under Python 3.9

This repo dogfoods itself. The published hook runs python3 -m pydependence, and -m puts
CWD first on sys.path -- so it was already executing the working copy inside a pinned
release's environment. That environment predates the 3.12 floor, so:

File "pydependence/_cli.py", line 31, in <module>
    from enum import StrEnum
ImportError: cannot import name 'StrEnum' from 'enum' (.../python3.9/enum.py)

Rather than wait for a new tag, the self-hook is now a local hook running the project
venv directly. That is what was effectively happening, minus the version skew, and it
removes the chicken-and-egg of needing a release to lint the code that produces it.

.pre-commit-hooks.yaml also gained language_version: "3.12" so consumers of the
published hook get a correct interpreter.

Two deviations from the shared config, both forced

No uv-lock hook and no committed uv.lock, and ty runs as a local hook rather than
via ty-pre-commit. pyproject.toml's bootstrapped example-legacy extra intentionally
pins pydantic<2, conflicting with pydantic>=2.0.0 in the base dependencies -- it is a
demo of this tool's env=-scoped version resolution, documented in the README, and never
meant to be installed. Any command that resolves the whole project (uv lock, uv sync,
uv check -- which ty-pre-commit shells out to) fails on it by design. uv's conflicts
marking only covers extra-vs-extra, not extra-vs-base. Running ty directly sidesteps
project resolution; uv pip install -e ".[test]" still works, so the shared lint workflow
is unaffected.

Also flagged, not fixed

pyproject.toml has a [tool.pytest] section where pytest expects
[tool.pytest.ini_options] -- as written it is silently ignored. Pre-existing and
unrelated, so left alone.

Testing

  • pre-commit run --all-files -> all 10 hooks pass
  • pytest -> 20 passed (19 original + 1 new regression test)
  • ruff check, ruff format --check, ty check -> clean

nmichlo and others added 6 commits September 9, 2026 09:53
Drops support for python 3.8-3.11 (breaking change for consumers): updates
`requires-python`, the `Programming Language :: Python` classifiers (now
3.12/3.13 only), and the `pydependence` pre-commit hook's `language_version`.
Also drops the now-unneeded `typing-extensions` runtime dependency, since
`typing.Annotated` has been in the stdlib since 3.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same config as mtg-dataset/disent/norfair-rs: line-length 120, `select =
["E4","E7","E9","F","I","UP","TID251","B009","B010"]`, single-line isort
imports, and a banned-api list for `typing.Any`/`typing.cast`.

Ran `ruff check --fix` and `ruff format`, then fixed the remainder by hand:
5 `str, Enum` classes to `StrEnum` (UP042), and a `TYPE_CHECKING`-guarded
`tomlkit` import so its quoted type annotations resolve (F821) now that the
runtime import stays lazy. The AST-fixture package `tests/test-packages/`
is excluded (`extend-exclude` + `force-exclude`): its files are fake modules
with imports to non-existent packages, parsed only via `ast.parse` by the
import-parser tests, never executed -- linting/formatting them would
"fix" away the exact bytes the tests assert on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same base as mtg-dataset: pre-commit-hooks v6.0.0, ruff-pre-commit v0.16.6.
No `uv-lock` hook / committed `uv.lock`, and the `ty` hook runs `ty` directly
via `uvx` instead of the standard `ty-pre-commit` hook (which shells out to
`uv check`): pyproject.toml's bootstrapped `example-legacy` extra intentionally
pins `pydantic<2`, conflicting with the `pydantic>=2.0.0` in the base
`[project.dependencies]`. Any `uv` command that resolves the full project
(`uv lock`, `uv sync`, `uv check`) fails on this by design -- it's a demo of
resolving conflicting `env=`-scoped versions, never meant to be installed as
a real extra. Kept the project's own `pydependence` self-hook, which
regenerates `pyproject.toml`'s dependency lists from `pydependence/`'s
own imports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds lint.yaml, test.yaml, release.yaml, each a thin caller of the shared
reusable workflows in nmichlo/.github, matching mtg-dataset. The old
python-lint.yml/python-publish.yml/python-test.yml/version-bump.yml split
(removed in the floor-bump commit) never worked for publishing: version-bump.yml
tagged on merge and python-publish.yml triggered on tag push, but a tag pushed
with GITHUB_TOKEN doesn't trigger another workflow. release.yaml owns both
triggers in one run, so it can actually publish.

release.yaml declares `permissions: contents: write` on the job, since a
reusable workflow can't hold more permission than its caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `[tool.ty]` (python 3.12, excluding the AST-fixture package for the
same reason as ruff) and gets `uvx ty@0.0.79 check` to "All checks passed!".

Real bug found: `from . import x` (a bare relative import, where
`ast.ImportFrom.module` is `None`) crashed `visit_ImportFrom` with
`TypeError: sequence item 1: expected str instance, NoneType found`
(pydependence/_core/module_imports_ast.py). `from .submodule import x` was
fine since `.module` is set there. Fixed and added a regression test.

The rest are `bool | None` / `str | None` config fields that are only
narrowed to non-`None` by a separate `set_defaults`/`apply_defaults` call
made before use -- added `assert ... is not None` at each use site to
document and check the invariant, since ty can't see across that call.
Also: converted 3 pydantic `@model_validator(mode="after")` classmethods to
plain instance methods (pydantic's stub only types the instance-method form),
fixed a `dict` key-type mismatch from a `None`-as-default-env convenience,
typed `_AstImportsCollector`'s node params as `ast.stmt | ast.expr` (not
`ast.AST`, which has no `lineno`/`col_offset`), and dropped the dead
python-3.8 `ast.unparse` polyfill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nmichlo
nmichlo merged commit 4405444 into main Sep 9, 2026
3 checks passed
@nmichlo
nmichlo deleted the chore/standardise-tooling branch September 9, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant