Skip to content

Enable ruff annotation rules (ANN) - #3905

Merged
anth-volk merged 12 commits into
masterfrom
ruff-annotations
Oct 16, 2026
Merged

anth-volk merged 12 commits into
masterfrom
ruff-annotations

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3904

Summary

Phase 1 step 1.8, group 3: annotations. Stacked on #3902.

  • Annotations: select gains ANN, and every function outside tests is now annotated: 252 findings in policyengine_api and the scripts, plus 16 in .github/.

  • Any (ANN401): replaced by a real type wherever one exists (56 of 63). Types used include object for opaque pass-through values, the existing JSONValue alias, CachedReformImpact, Starlette Scope, ArrayLike, and two new Protocols (CachePipeline, SPMCapabilitySource).

  • Remaining suppressions: each keeps an inline # noqa: ANN401 with a reason:

    • exact-type() JSON validators mypy can't narrow;
    • Session.exec overloads that reject core selects;
    • dataclasses.replace(**changes);
    • the TOML value from Enforcer.get;
    • incompatible Flask/a2wsgi WSGI stubs;
    • pass-through JSON values;
    • Pydantic model-class maps.

    One noqa: ANN201 is on a Pydantic wrap serializer, because annotating it would change the published OpenAPI schema.

  • Test-file exemptions: **/tests/** ignores S101, D1, ANN and PLR2004, as agreed for step 1.8. This is registered as api.quality.tests-ruff-exemptions.

  • Annotations checked against the code: I ran mypy over the whole package (beyond the configured v2 scope) and compared it with the base branch. Annotations that didn't match the code were corrected in the commit "Correct annotations that whole-package mypy shows were wrong". Whole-package mypy errors fell from 259 to 182.

  • Eight new whole-package errors remain:

    • Library typing: policyengine_core has no py.typed, and Connector.connect and numpy arithmetic return Any.
    • Two real code gaps, now visible because the functions are annotated:
      • Flask's view_args is optional, but the route indexes it.
      • get_execution_result accepts None, but its success path needs an execution.

    Fixing either needs a code change, which is out of scope for an annotation PR.

  • Runtime-annotation guard: the guard Enable ruff typing-only import rules (TC) #3902 added now resolves each model's bases the way ruff does: through the import in the model's own module, following bases defined in the same module. Ruff matches that import path, not where a base is defined, so a listed base imported through a re-export doesn't count. The old guard compared definition paths and would have missed that case. A new test pins the resolution, and an experiment against ruff 0.15.12 confirmed each case. No model in the package is affected today. The rule statement and docs/engineering/skills/typing-only-imports.md now describe the matching precisely.

  • PolicyEngine bundle 6.4.1 (replaces Update PolicyEngine bundle to 6.4.1 #3939): the Simulation API now runs 6.4.1 (chore(deps): update policyengine to 6.4.1 policyengine-sim-api#809). On 6.3.4, the Public API's "Ensure PolicyEngine bundle is supported by simulation API" check fails with "The us route resolves to a different worker application", which also blocks master's staging deploy. The bundle moves policyengine-uk from 2.126.12 to 2.130.1; US stays at 2.2.1.

    • Catalog counts reviewed and updated in test_v2_catalog_installed.py. UK adds 37 variables and removes one, and adds parameters for ESA and JSA remunerative work, the tax credits Pension Credit passport and the Universal Credit company-owner rules.
    • Removed input: policyengine-uk 2.129.0 removed uc_has_limited_capability_for_work in favour of uc_limited_capability_for_work. It joins DEPRECATED_VARIABLES, so a partner still sending it gets a warning naming the replacement instead of a failed calculation.

Testing

  • pytest tests/unit tests/contract: 2,358 passed. to_refactor plus dedupe: 53 passed.
  • Configured mypy is clean. ruff check/format are clean repo-wide.
  • All 234 Pydantic model schemas and the OpenAPI spec are identical to the base branch.
  • Guards and the rules tool unittest pass. scripts/rules is byte-identical with sim.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3888 October 13, 2026 12:35
@codecov

codecov Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.71429% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.30%. Comparing base (784b753) to head (d7ce964).

Files with missing lines Patch % Lines
policyengine_api/runtime_cache/core.py 22.22% 0 Missing and 14 partials ⚠️
policyengine_api/spm.py 66.66% 4 Missing and 1 partial ⚠️
policyengine_api/asgi_factory.py 62.50% 3 Missing ⚠️
policyengine_api/specification.py 57.14% 2 Missing and 1 partial ⚠️
...e_api/utils/payload_validators/validate_country.py 66.66% 2 Missing and 1 partial ⚠️
policyengine_api/worker_spm.py 57.14% 1 Missing and 2 partials ⚠️
policyengine_api/data/orm.py 50.00% 1 Missing and 1 partial ⚠️
policyengine_api/data/v2/catalog/extraction.py 60.00% 1 Missing and 1 partial ⚠️
...pi/fastapi_routes/v2/households/document_models.py 60.00% 1 Missing and 1 partial ⚠️
policyengine_api/libs/gateway_auth.py 60.00% 1 Missing and 1 partial ⚠️
... and 5 more
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3905       +/-   ##
===========================================
+ Coverage   47.76%   85.30%   +37.54%     
===========================================
  Files         239      255       +16     
  Lines       13374    14724     +1350     
  Branches     2300     2599      +299     
===========================================
+ Hits         6388    12561     +6173     
+ Misses       6363     1337     -5026     
- Partials      623      826      +203     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@anth-volk
anth-volk removed this pull request from stack #3888 October 13, 2026 19:26
@anth-volk
anth-volk added this pull request to stack #3917 October 13, 2026 19:26
@anth-volk
anth-volk force-pushed the ruff-annotations branch 7 times, most recently from cd4765b to 4c0a7c6 Compare October 14, 2026 21:13
@anth-volk
anth-volk force-pushed the ruff-annotations branch 5 times, most recently from 1403bc2 to 28faa7d Compare October 15, 2026 00:06
Base automatically changed from ruff-typing-imports to master October 15, 2026 15:07
anth-volk and others added 4 commits October 16, 2026 09:03
Type the cache backend protocol with what callers rely on (string
envelopes in, opaque write acknowledgements out) and add a CachePipeline
protocol for the queued writes. Decoded envelope payloads are object,
because every consumer validates them. A SPMCapabilitySource protocol
covers both the HTTP entrypoint client and the release registry adapter
that validate_worker_spm accepts. ReformImpactCache.update keeps Any for
the keyword changes it forwards unchanged to dataclasses.replace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reform-impact service parameters take the cache's scalar types, with the
options hash optional on the write paths that receive it from
EconomicImpactSetupOptions. Policy JSON read from the v1 table is a
JSONValue. SPMSelection.serialize_selection stays without a return
annotation (noqa ANN201) because Pydantic would build the model's
serialization JSON schema from it and change the published OpenAPI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before-validators take and return object. The parameter-tree SQL helpers
take mapped columns and return typed column expressions. Three helpers
keep Any with a reason: the JSON validators and canonical encoders
dispatch on exact type() checks mypy cannot narrow, and read_rows passes
core SQLAlchemy selects that Session.exec's overloads do not declare.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View decorators are generic over the wrapped view's result, so a view
returning dict | Response keeps that type plus the error Response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 8 commits October 16, 2026 09:03
scripts/rules stays standard-library only. Enforcer.get keeps Any for
the raw TOML value that callers read by its shape. scripts/rules changes
need mirroring in policyengine-sim-api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select ANN everywhere except tests, which need no annotations. The
annotation commits before this one fix every finding outside .github/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bump_version.py, find-api-model-versions.py and scripts/check_updates.py
(with a ChangelogEntry TypedDict). Annotations only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api.quality.ruff-lint now includes ANN, and the **/tests/** per-file
ignore list (S101, D1, ANN, PLR2004) is registered as
api.quality.tests-ruff-exemptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruff treats a class as runtime-evaluated when a base resolves, through
the import in the class's own module, to a listed name, or when a base
defined in the same module is itself runtime-evaluated. It matches the
import path, not where the base is defined, so a listed base imported
through a re-export is not recognised. The guard compared definition
paths, so it would have passed such a subclass while ruff asked for its
annotation imports to be moved.

The guard now parses each model's module, resolves its bases through
the module's imports (including relative imports, attribute access and
subscripted generics), follows same-module bases, and fails with the
path to add. test_base_resolution_matches_ruff pins the resolution:
listed imports, module attributes, same-module chains, generics and
relative imports count; re-exports and unlisted third-party bases do
not. An experiment against ruff 0.15.12 confirmed each case. No model
in the package is affected today. The rule statement and the
typing-only-imports skill describe the matching precisely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 6.4.1 bundle on master moves policyengine-uk to 2.130.1.
policyengine-uk 2.129.0 removed the input
uc_has_limited_capability_for_work in favour of
uc_limited_capability_for_work, so a partner still sending it would now
fail. It joins DEPRECATED_VARIABLES: it is dropped with a warning
naming the replacement, as for medical_out_of_pocket_expenses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The good fixture for api.v2.user-services-no-sql had no annotations:
the only ruff group 3 findings in the code #3891 added. It now takes the
session and returns the user's policies with the same types as the bad
fixture. It still builds no SQL itself, so the rule's proof still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk marked this pull request as ready for review October 16, 2026 06:44
@anth-volk
anth-volk requested a review from a team as a code owner October 16, 2026 06:44
@anth-volk
anth-volk merged commit 69cb5ae into master Oct 16, 2026
15 checks passed
@anth-volk
anth-volk deleted the ruff-annotations branch October 16, 2026 06:44

This branch was successfully deployed

1 active deployment
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.

Enable ruff annotation rules (ANN)

1 participant