Skip to content

Runtime annotation introspection raises in tests/workflow_contracts #730

Description

@leynos

tests/workflow_contracts annotates several functions with names imported only under typing.TYPE_CHECKING. Introspecting those annotations at runtime raises NameError. This records the position reached on #728 and #729, where reviewers asked for a change on both, so the next reviewer does not reopen it from scratch.

The position

Four facts, each measured rather than assumed.

The annotations do not fail at import. PYTHON_BASELINE is 3.14, where PEP 649 defers annotation evaluation. Both affected modules import, and the workflow-contract suite that imports them passes:

$ python3 -c "import whole_run_ordering, nextest_budgets"
both modules import cleanly under 3.14.3

The future import is forbidden here. The house lint refuses it by name:

tests/workflow_contracts/whole_run_ordering.py:13:0: C9112: Remove
'from __future__ import annotations' on a 3.14+ baseline
(redundant-future-annotations)

No module in this repository uses it, which is consistent with the rule rather than an oversight. Applying the reviewers' suggested fix reds make lint-python.

The placement is ruff's own requirement. typing-only-standard-library-import fires on a runtime import used only in annotations, so moving the import back out trades one gate failure for another.

The introspection failure is real, and older than either pull request. It is true on main today for cabc.Iterable[CoverageLane] in whole_run_ordering, from well before this work:

>>> whole_run_ordering.watchdog_required_for.__annotations__
NameError: name 'fractions' is not defined
>>> typing.get_type_hints(whole_run_ordering.watchdog_required_for)
NameError: name 'fractions' is not defined

Nothing in the repository introspects them. ty reads them statically and passes.

What would not fix it

The obvious candidate is extending lint-workflow-scripts, which loads every module under .github/scripts and whose recipe comment says that "only loading them catches a definition-time failure such as an annotation naming a TYPE_CHECKING-only import", to walk tests/ as well.

Measured on a minimal module with exactly this shape, it would not catch it:

--- runpy.run_path, the loader gate's own mechanism ---
loaded with no error
--- accessing __annotations__ ---
NameError: name 'fractions' is not defined
--- typing.get_type_hints ---
NameError: name 'fractions' is not defined

The loader's mechanism is what PEP 649 made insufficient. Worth noting separately: that makes the recipe comment stale for the 3.14 baseline. Loading a module no longer catches this class, so the gate is still useful for other definition-time failures but not for the one it names.

What would

A check that resolves the annotations rather than merely loading the module: walk each module's public functions and call typing.get_type_hints on each, which is what the two runs above show to be the discriminating operation. That is a different gate from the loader, not an extension of it, and it would fail on main today until the existing annotations are addressed, so adopting it means deciding what to do about those at the same time.

Three ways to address them, none obviously right, which is why this is an issue rather than a change:

  1. Import the names at runtime and suppress typing-only-standard-library-import where they are annotation-only. Costs a suppression per module and a small import cost.
  2. Quote the annotations. Keeps the lint clean and makes get_type_hints work, at the cost of the quoting being invisible to a reader as a deliberate choice.
  3. Decide that runtime introspection is not a use these modules support, and write that down instead of adding a gate. They are workflow contracts read by ty and by pytest, neither of which introspects.

Not urgent

Nothing reads these annotations at runtime, every gate passes, and both #728 and #729 are unaffected in behaviour. This is a latent sharp edge in a helper plus a stale comment, recorded so the position is citable.

Raised out of the review rounds on #728 and #729, which each declined the change on the grounds above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglowBacklog work, hygiene, or technical debt planned opportunistically, without a strict deadline.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions