Skip to content

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

Draft
anth-volk wants to merge 21 commits into
masterfrom
ruff-structure-imports
Draft

anth-volk wants to merge 21 commits into
masterfrom
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. The lowest PR in its stack; #3924 builds on it.

  • 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. They also ignore N802, because test fakes name their methods after the interface they replace (a gRPC stub's Export). 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. The kept ones are listed below.
  • 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.

    • Listed as lazily loaded (44 suppressions removed): pyproject.toml lists 23 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 and the nine v2 services modules, which load persistence only when a default factory runs;
      • libs.auth0_management and google.cloud.secretmanager, which load only when a secret is read. data/orm.py imports a different submodule, google.cloud.sql.connector, at module level, so only the Secret Manager client is listed.

      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.

      Two v2 services imported sibling services at module level. Those imports now sit in the one function that uses each name: V2AccountService.export_account (four read_all_user_* readers) and mirror_legacy_user_policy_in_session (mirror_legacy_policy_in_session). Both modules already loaded only lazily, and no test patches those names.

    • Kept (16): see "Inline suppressions".

  • Namespace packages (INP001, 45): policyengine_api has no __init__.py on purpose, and .github, 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 the hyphenated .github/find-api-model-versions.py (N999). A shell script runs it by that path, so N999 joins the .github/** file-level exceptions (api.quality.ci-cd-scripts-ruff-exemptions). 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. Three reads of another object's private members now use public names, with no change in behaviour:
    • the in-memory cache pipeline reads its backend's lock through a new read-only operation_lock property;
    • ReportOutputService calls SimulationService.ensure_simulation_dual_write_state, formerly _ensure_simulation_dual_write_state, which now has a docstring;
    • worker_spm_release calls the new constants.get_bundle_manifest(), which reads the module attribute at call time, so tests that patch constants._policyengine_bundle still take effect.
  • 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. Two are kept; see "Inline suppressions".
  • Registry: api.quality.ruff-lint now lists the seven families and TID253. The test and .github exemption rules list the new codes. 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 lazy-import mechanism.

Inline suppressions

This PR adds 19 and removes none (78 on master, 97 here). For each one, no code change, ruff setting or allowed file-level exception removes the finding without changing behaviour (api.process.agents-suppress-as-last-resort).

PLC0415, 16. Another application module imports each of these modules at module level, so it can't be listed in banned-module-level-imports. For country, metadata_service and specification, that importer is a single Flask module (services/metadata_service.py, routes/metadata_routes.py and routes/system_routes.py). Listing them would mean deferring imports in Flask code, which loads them at startup anyway.

Location Imports Reason
fastapi_routes/dependencies.py:704 libs.simulation_entrypoint builds its client at import, which needs the gateway URL configured
fastapi_routes/dependencies.py:712 services.metadata_service loads every country model; only the default reader needs it
fastapi_routes/dependencies.py:720 specification reads and builds the OpenAPI document at import
fastapi_routes/dependencies.py:805 data.orm looked up per call, so tests can replace the v1 session factory
fastapi_routes/v2/metadata/common.py:82 fastapi_routes.dependencies looked up per request, so tests can replace the default factory
libs/simulation_entrypoint.py:260 constants constants reads the installed bundle manifest at import
spm.py:460, spm.py:477 constants constants reads the installed bundle manifest at import
services/household_calculation_service.py:134, :184 country country imports this module and loads every country model
warmup.py:49 country country models load during warmup, not at import
readiness.py:19, :25, :28, :34 data.v2.settings, libs.user_auth, migration_flags, spm keeps importing readiness cheap; only validation needs configuration
services/v2/policies/validators.py:100 services.v2.policies.types types imports this module (a cycle)

Others, 3:

Location Code Reason
country.py:699 N801 class reform: policyengine-core uses the class name as the reform key
services/v2/metadata/services.py:19 E501 a dotted module path can't be wrapped
scripts/measure_cloud_run_runtime.py:12 E501 the docstring is the script's --help text. Its 93-character first sentence can't be wrapped without breaking the docstring rules (D205, D400), and rewording it would change the help output

Testing

  • pytest tests/unit at the head (before the changelog and comment-only commits): 2,686 passed.
  • mypy over [tool.mypy].files is clean (167 source files).
  • 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 were byte-identical to the base, with and without OLD_SIMULATION_GATEWAY_URL set. The later commits change no route, model or schema.
  • The quality guards, the rules guard and the rules tool unittests pass.
  • 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 after this PR's last changes: ruff check (including RUF100 from Enable ruff argument, idiom and pytest-style rules (ARG, B, C4, FBT, PERF, PLW, PT, PTH, RUF, SIM and others) #3924 up, so no suppression is left unused) and ruff format --check are clean, and the quality guards and rules guard pass. The PRs above rebased with no conflicts outside pyproject.toml, the registry and the rendered rules. None of them refers to the three renamed private names.
  • CI/CD: no workflow changed. .github/find-api-model-versions.py lost its suppression comment, and no environment variable was added, removed or renamed.
  • CI: this PR is now the lowest in its stack, so it runs the full PR workflow. The run for the current head is queued.

🤖 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 5 times, most recently from a8c5058 to f7461fb Compare October 19, 2026 12:28
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch from f7461fb to 54bb019 Compare October 19, 2026 13:39
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch 2 times, most recently from d86f471 to b3fa9cd Compare October 19, 2026 18:48
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch from b3fa9cd to 955d8d5 Compare October 19, 2026 19:14
Base automatically changed from ruff-security-logging to master October 19, 2026 21:14
anth-volk and others added 16 commits October 20, 2026 01:14
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>
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>
#3915 removed dashboard/experiments, so ruff no longer needs to treat it
as a namespace package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits October 20, 2026 01:30
No application module imports google.cloud.secretmanager or the households,
legacy_refs, metadata and user_accounts v2 services modules at module level
(data/orm.py imports a different submodule, google.cloud.sql.connector). Listing
them in banned-module-level-imports lets PLC0415 accept their nine in-function
imports, so their suppressions go. The imports that were wrapped only to fit a
suppression are back on one line.

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

The policies, user_policies, user_households, user_reports and
user_simulations services modules were imported at module level only by two
sibling services that load lazily themselves: user_accounts (in
export_account) and user_policies (in mirror_legacy_user_policy_in_session).
Those imports now sit in the one function that uses each name, and the five
modules are listed in banned-module-level-imports, so their seven
suppressions go.

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

Three call sites reached into another object's private members (SLF001):

- InMemoryPipeline read its backend's _lock. The backend now exposes it as
  operation_lock, a read-only property.
- ReportOutputService called SimulationService._ensure_simulation_dual_write_state.
  The method is now public as ensure_simulation_dual_write_state, with a
  docstring.
- worker_spm_release read constants._policyengine_bundle. constants now has
  get_bundle_manifest(), next to its other get_bundle_* functions. It reads
  the module attribute at call time, so tests that patch the attribute still
  take effect.

Behaviour is unchanged, and the three suppressions go.

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

codecov Bot commented Oct 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.85714% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.29%. Comparing base (3f7deea) to head (94e0027).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
policyengine_api/fastapi_routes/dependencies.py 8.00% 23 Missing ⚠️
policyengine_api/services/economy_comparison.py 0.00% 8 Missing ⚠️
policyengine_api/readiness.py 0.00% 4 Missing ⚠️
policyengine_api/country.py 0.00% 2 Missing ⚠️
policyengine_api/services/economy_service.py 0.00% 2 Missing ⚠️
...gine_api/services/household_calculation_service.py 0.00% 2 Missing ⚠️
policyengine_api/services/household_mirroring.py 0.00% 2 Missing ⚠️
policyengine_api/services/policy_mirroring.py 0.00% 2 Missing ⚠️
policyengine_api/services/user_policy_mirroring.py 0.00% 2 Missing ⚠️
...gine_api/services/v2/comparison_runs/validators.py 0.00% 2 Missing ⚠️
... and 11 more

❗ There is a different number of reports uploaded between BASE (3f7deea) and HEAD (94e0027). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (3f7deea) HEAD (94e0027)
2 1
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3920       +/-   ##
===========================================
- Coverage   84.08%   46.29%   -37.79%     
===========================================
  Files         255      239       -16     
  Lines       15368    13971     -1397     
  Branches     2555     2267      -288     
===========================================
- Hits        12922     6468     -6454     
- Misses       1686     6937     +5251     
+ Partials      760      566      -194     

☔ 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 and others added 2 commits October 20, 2026 01:40
Both findings fall where the registry allows file-level exceptions, and both
are the norm there: update-policyengine-package.sh runs
.github/find-api-model-versions.py by its hyphenated name, and test fakes name
their methods after the interfaces they replace (the gRPC stub's Export). The
.github/** and **/tests/** entries, and their registry rules, now list N999 and
N802, and the three inline suppressions go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the ruff-structure-imports branch from 6b2799b to 06de181 Compare October 19, 2026 21:40

This branch was successfully 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