Skip to content

Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) - #3920

Draft
anth-volk wants to merge 15 commits into
ruff-security-loggingfrom
ruff-structure-imports
Draft

anth-volk wants to merge 15 commits into
ruff-security-loggingfrom
ruff-structure-imports

Conversation

@anth-volk

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

Copy link
Copy Markdown
Collaborator

Fixes #3919

Summary

Phase 1 step 1.8, group 7: structure and imports. Stacked on #3915.

  • Test exemption (decided for this group): test files now also ignore SLF001, INP001 and PLC0415, alongside S101, D1, ANN and PLR2004. Tests deliberately reach into private members, pytest test directories aren't packages, and tests often import inside a test after patching the environment. Application code still gets every rule. api.quality.tests-ruff-exemptions is updated to match.
  • Rules: select gains A, E, FAST, INP, N, PLC and SLF. All 289 findings left after the exemption are fixed or kept with an inline reason.
  • FastAPI dependencies (FAST002, 21): ruff's own fix would break the v2 routes, which use from __future__ import annotations: FastAPI resolves string annotations against module globals, and the query dependencies were local closures. Each dependency is now a module-level alias instead, such as HouseholdCreateQueryDependency = Annotated[HouseholdCreateQuery, Depends(query_dependency(HouseholdCreateQuery))]. The OpenAPI is byte-identical.
  • Imports inside functions (PLC0415):
    • Moved (8, plus 4 in the code Stage 12A: Sign-in identity and v1-anchored user associations #3891 added): imports that add no cycle, side effect or third-party load. A fresh-interpreter check of all 220 policyengine_api modules shows that policyengine_api.asgi, policyengine_api.api and 211 others load exactly the same modules. Seven route and dependency modules now also load the small, dependency-free readiness and fastapi_routes.dependencies modules.

    • Allowlisted as lazily loaded (28 suppressions removed): pyproject.toml lists 13 packages and modules in ruff's banned-module-level-imports, with TID253 selected:

      • policyengine and spm_calculator, which load country models;
      • data.v2.database, which registers an exit hook at import;
      • the nine v2 database_session modules;
      • libs.auth0_management.

      No application module imports any of them at module level. Importing one there now fails, though TYPE_CHECKING imports are fine, and PLC0415 accepts their in-function imports without a suppression. Tests and .github scripts may still import them at module level, so TID253 joins their file-level exceptions; find-api-model-versions.py imports policyengine.

    • Kept (32), each with a reason: modules that some application module also imports at module level, so they can't be allowlisted:

      • Google clients (data/orm.py imports google.cloud);
      • the v2 services modules, which sibling services import;
      • import-time I/O (simulation_entrypoint, specification, constants);
      • country models (country, metadata_service);
      • readiness's configuration imports;
      • one cycle;
      • two places where tests patch the name per call.
  • Namespace packages (INP001, 45): policyengine_api has no __init__.py on purpose, and .github, dashboard/experiments, migrations/v1, migrations/v2 and rules/fixtures aren't packages. They're declared in ruff's top-level namespace-packages setting, with a comment. Compared with the full ruff check output before the change, the only other difference is a hyphenated .github script name (N999), kept with a reason because a shell script runs it by that path. No __init__.py was added, and the built wheel and sdist contain the same files.
  • Names (N, A, SLF001): internal names are renamed: two constants in economy_comparison.py, the rules tool's BaseUnavailableError and assert_violation, SQLAlchemy's TimeoutError imported as PoolTimeoutError in tests, and 13 test function names that alone overflowed the line. Kept with a reason: country.py's class reform, since policyengine-core uses the class name as the reform key; two gRPC-stub Export methods in a test; and three private accesses with no public equivalent that tests patch.
  • Line length (E501, 149): wrapped. Every split string has identical text, which a syntax-tree comparison confirms. The two published Field descriptions that were split are unchanged. Kept with a reason (6 findings, 4 suppressions): a dotted module path, two GPT-4 prompts in the dashboard experiment, and a docstring line that is a script's --help text.
  • Registry: api.quality.ruff-lint now lists the seven families and TID253. The rules tool changed (wrapped lines and the renames above), and sim takes the same changes in Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) policyengine-sim-api#799, which has the same allowlist mechanism.

Testing

  • pytest tests/unit tests/contract, to_refactor and the dedupe integration test (the make test set): 2,420 passed, the same as the base. That run was before the stack was rebased onto the release commit; this branch differs from it only in that commit's CHANGELOG.md, pyproject.toml version, uv.lock and changelog fragment.
  • make typecheck-v2 is clean. mypy on the 65 changed files outside that gate gives the same output as the base.
  • ruff check and ruff format --check (0.15.12) are clean repo-wide.
  • Every Pydantic model schema, the FastAPI OpenAPI with every native router enabled, the /v2/openapi.json preview and the legacy spec are byte-identical to the base, with and without OLD_SIMULATION_GATEWAY_URL set.
  • The quality guards, the rules guard and the rules tool unittest pass.
  • For the allowlist change, at each tip from Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) #3920 to Register weighted-microdata rules, with AST guards and #767 proofs #3938:
  • CI/CD: no workflow changed. .github/find-api-model-versions.py gained only a suppression comment.
  • CI: this PR is above the bottom of its stack, so it runs only CI complete, which fails and says why. Its full run starts once the PRs below it merge.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3917 October 14, 2026 13:55
@anth-volk anth-volk changed the title ruff structure imports Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) Oct 14, 2026
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch 3 times, most recently from a346195 to 0f717e8 Compare October 14, 2026 18:08
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch 8 times, most recently from 5b47c96 to b5f847b Compare October 15, 2026 00:06
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch from b5f847b to 551bc44 Compare October 15, 2026 15:21
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch 2 times, most recently from 015e6fc to 428c6f9 Compare October 15, 2026 17:21
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch 3 times, most recently from eb79057 to ccc3096 Compare October 16, 2026 06:45
anth-volk and others added 4 commits October 16, 2026 11:05
Tests deliberately reach into private members, pytest test directories
aren't packages, and tests often import inside a test after patching the
environment. Application code still gets every rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route modules use `from __future__ import annotations`, and FastAPI
resolves string annotations against module globals only, so the query
dependencies move from per-router closures to module-level Annotated
aliases. The OpenAPI documents are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename the purely internal names: the outcome bounds and labels in the
decile impact helpers, the rules tool's BaseUnavailableError and its
tests' assert_violation helper, a test's list of uncalculated variables,
the PostgreSQL UUID import, and the tests' SQLAlchemy TimeoutError
imports (now PoolTimeoutError, so they no longer shadow the builtin).

Keep, with reasons, the Reform subclass name that policyengine-core uses
as the reform key, the gRPC stubs' Export methods, and the three private
accesses that have no public accessor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine_api is a namespace package (its routes and libs directories
included), and .github, dashboard/experiments, the two Alembic
environments and rules/fixtures hold scripts and fixtures that are never
imported as packages. Listing them in ruff's namespace-packages changes
no other rule's result, except N999 on the hyphenated
.github/find-api-model-versions.py, which keeps its name because
update-policyengine-package.sh runs it by that path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 11 commits October 16, 2026 11:05
Move four imports out of functions: request_context now reads
get_runtime through the observability package it already imports, the
simulation entrypoint imports SPMValidationError at the top (spm was
already loaded), the four v2 resource routers import their default
service factories, and the route dependencies import is_ready. None
creates a cycle or loads a third-party or standard-library module that
was not already loaded; the ASGI and Flask entry points import exactly
the same modules as before.

The remaining deferred imports keep an inline reason: they load country
models, PolicyEngine.py, spm_calculator, Google clients, the v2
persistence stack (whose database module registers an atexit hook) or
the bundle manifest, break an import cycle, need configuration at
import, or are looked up per call so tests can replace them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Long strings are split by implicit concatenation, so every log message,
error message and published Field description keeps its exact text;
long comments are rewrapped or moved above the line they describe. Only
four docstrings change, none of them published: a Flask route's, two
internal functions' and a rule proof fixture's.

Three lines keep a suppression: a dotted module path that cannot be
wrapped, and the GPT-4 prompts and a script's --help summary, whose text
would change if wrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every split string keeps its exact text (the parsed code is unchanged),
so the rendered rules page and the AGENTS.md block are identical apart
from the tool hash. Two test docstrings are reworded to fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split long strings (same text), rewrap comments and reword over-long
test docstrings. Thirteen test functions whose names alone overflowed
the line get shorter names; nothing references them. The economy
service tests reach the long-named reform-impact lookup mock through a
small hash_prefix_lookup helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select A, E, FAST, INP, N, PLC and SLF. The earlier commits in this
branch fix or explain every finding, so ruff reports none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api.quality.ruff-lint now lists A, E, FAST, INP, N, PLC and SLF. The
rendered rules page also picks up the rules tool's new hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The /v2/me, user-report, user-simulation, user-household and user-policy
routes that #3891 added or reshaped took 44 dependencies as parameter
defaults. Their query dependencies become module-level Annotated
aliases, as the other v2 routes do, and the request-size check and the
upgrade route's unauthenticated-user header become Annotated
parameters.

Each handler's principal comes from require_user, the authenticator
that NativeRouteDependencies injects per router. FastAPI resolves string
annotations against module globals only: with `from __future__ import
annotations`, Annotated[AccountPrincipal, Depends(require_user)] would
silently become a required "principal" query parameter. These five
modules therefore no longer use the future import, so their annotations
are evaluated where the handlers are defined, and import the route
dependency types and their default service factory at run time. Moving
the default factory import out of _service_factory also clears its
PLC0415 finding in the three new modules, as the other v2 resource
routers already do; the dependencies module is already loaded wherever
these routers are built.

The OpenAPI documents, with the injected test authenticator and with the
default one, are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Of the 18 PLC0415 findings left in the code #3891 added, the route
dependencies' account-request authenticator import moves to the top
level. Its module is already loaded wherever the dependencies module is,
and it creates no cycle; the ASGI and Flask entry points import exactly
the same modules as before.

The other deferred imports keep an inline reason, worded as for the
existing ones: the v1 session factory is looked up per call so tests
can replace it; the v1 reference checks, v2 persistence, the
user-account service and Auth0 Management load only when a default
factory runs or a v1 reference is checked; readiness validation loads
the user-auth and Auth0 settings only when it runs; and the Google
Secret Manager client loads only when Auth0 Management reads a secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user-service proof fixture's docstring puts the rule's verdict on
its own line, as the validator fixtures do. The import side-effect
test's subprocess script asserts the association qualification module
is unloaded across three lines, and the identity persistence test's
qualification test names the qualification module once, as
qualification, so its pending checker fits on one line. The checks are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typing-only-imports skill now says why the five signed-in user route
modules don't use `from __future__ import annotations`: their handlers
depend on the authenticator each router is given, which FastAPI cannot
resolve from a string annotation against module globals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mport

Thirteen packages and modules are only ever imported inside the function
that needs them:
- policyengine and spm_calculator, which load country models;
- the v2 database module, which registers an exit hook at import;
- the nine v2 database_session modules, which open persistence only when
  a default factory runs;
- Auth0 Management, which loads the Google client.

No application module imports any of them at module level.

pyproject.toml now lists them in banned-module-level-imports and selects
TID253. Importing one at module level fails (TYPE_CHECKING imports are
still fine), and PLC0415 accepts their in-function imports without a
suppression. That removes 28 PLC0415 suppressions.

Tests and CI/CD scripts may still import them at module level: TID253
joins their file-level exceptions, since find-api-model-versions.py
imports policyengine.

Modules that some application module does import at module level keep
their inline reasons. These include google.cloud, the v2 services
modules, constants and country.

Comments and configuration only: no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch from ccc3096 to a8c5058 Compare October 16, 2026 07:05

This branch has not been deployed

No deployments
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 structure and import rules (INP, PLC, SLF, N, A, FAST, E)

1 participant