Skip to content

Enable ruff exception rules (TRY, EM, BLE) - #3913

Draft
anth-volk wants to merge 12 commits into
ruff-docstringsfrom
ruff-exceptions
Draft

anth-volk wants to merge 12 commits into
ruff-docstringsfrom
ruff-exceptions

Conversation

@anth-volk

Copy link
Copy Markdown
Collaborator

Fixes #3912

Summary

Phase 1 step 1.8, group 5: exceptions and messages. Stacked on #3907.

  • Rules: select gains TRY, EM and BLE. All 1,337 findings at the base are fixed.
  • Messages (EM101 407, EM102 226, TRY003 605): ruff's fix builds each message in a variable before the raise (633 places). A script put every message back inline in both trees: all 122 files came out identical, so the message text is unchanged. It also found no clash with an existing msg in any scope.
    • Error codes renamed: where the first argument of SPMValidationError is an error code, the hoisted variable is named error_code, not msg (18 places).
  • Raises inside try blocks (TRY301 32, TRY300 1): each raise moves into a small helper, so a handler can't catch its own raise. Each handler still receives the same exceptions. The helpers live in the simulation entrypoint, the worker SPM client, spm, the economy service, the routes and one test.
  • Exception types:
    • TRY002 (5): the v1 services raise named exceptions, such as InvalidPolicyIdError, instead of bare Exception. Each subclasses Exception directly, and Flask's generic handler still renders str(error), so responses are unchanged.
    • TRY004 (17): 10 type checks now raise TypeError, where nothing catches the old type specifically or the only handler treats both alike. 7 keep their type with a reason:
      • Two Pydantic validators keep ValueError. TypeError would turn a validation error into a 500.
      • Four spm checks keep ValueError, because calculation_spm_receipt catches only ValidationError and ValueError.
      • constants.py keeps RuntimeError, which a test asserts.
    • TRY201 (3): raise e becomes a bare raise in the reform-impact persistence handlers.
  • Broad except Exception catches (BLE001 38):
    • Narrowed (1): country.py now catches only the three errors float() can raise.
    • Kept (37), each with an inline reason: most are telemetry, cache and logging code that must never fail a request. The rest are a health probe, route-level catch-alls and a mirror safety net after a committed write. None of them already logged in a form ruff accepts, and adding logging would change log output.
    • Older suppressions: ten noqa: BLE001 comments, inert until now, used the - reason style. They now use the standard # reason form.
  • Log output: the three household calculation error logs (TRY401) use a fixed message instead of repeating str(error); the traceback still ends with the exception and its message. Elsewhere, tracebacks gain a frame for each new helper and lose the extra raise e line.
  • Published schemas: byte-identical to the base. That covers 234 Pydantic models, 104 TypeAdapters, the FastAPI OpenAPI with every native router enabled, the /v2/openapi.json preview and the legacy spec.
  • Registry: api.quality.ruff-lint now lists BLE, EM and TRY. The shared rules tool needed no changes and is still byte-identical with sim (tool sha256 db83d5ad2aa4). Sim's group 5 is Enable ruff exception rules (TRY, EM, BLE) policyengine-sim-api#783.

Testing

  • pytest tests/unit tests/contract, to_refactor and the dedupe integration test (the make test set) at this branch's tip: 2,420 passed.
  • make typecheck-v2 is clean. mypy on the 15 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.
  • The quality guards, the rules guard and the rules tool unittest pass.
  • CI: this PR is above the bottom of its stack, so it runs only CI complete, which fails and says why (step 0.7, Make CI enforcement trustworthy before the rules registry #3887). Its full run starts once the PRs below it merge.

🤖 Generated with Claude Code

anth-volk and others added 12 commits October 14, 2026 21:57
Apply ruff's EM101/EM102 fix: every raise that built its message inline
now assigns it to msg first, which also clears TRY003. Message text is
unchanged; an AST check inlined each msg back into its raise and found
the trees identical, and confirmed no function gaining msg already read,
bound or closed over another msg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checks that get_spm_capability, validate_worker_spm,
validate_worker_result, normalize_spm_selection and
calculation_spm_receipt wrapped in try blocks now live in helpers, so each
handler still receives the same exceptions (TRY301).

A failed isinstance check raises TypeError where every handler of it
already treats TypeError and ValueError alike, or nothing catches it
(TRY004). It stays ValueError where calculation_spm_receipt certifies
only ValueError or a Pydantic validator reports it, and RuntimeError where
the bundle tests expect it, each with an inline reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The household, policy, report output and simulation services raise
InvalidHouseholdIdError, InvalidPolicyIdError, InvalidReportOutputIdError
and InvalidSimulationIdError instead of a bare Exception (TRY002). Each
subclasses Exception directly, so every existing except Exception and the
Flask generic error handler still catch them, with the same message.

The budget-window method moves its stored-failure replay and its
submission-claim loop into helpers, so their raises leave the logging try
block (TRY301), and the reform-impact persistence handlers re-raise with
a bare raise (TRY201). Budget-window SPM output whose annual impacts are
not a list of objects raises TypeError, which its only handler already
treats like ValueError (TRY004).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report-output and simulation PATCH routes check for "No fields to
update" in a helper, so the BadRequest leaves the try block yet still
reaches the same HTTPException re-raise (TRY301). The household
calculation routes log a fixed message with the traceback instead of
passing the exception object to logging.exception (TRY401); the
traceback still ends with the exception and its message.

The household create request's model validator keeps raising ValueError,
which Pydantic reports as a 422, with an inline reason (TRY004).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find-api-model-versions.py raises TypeError when the installed bundle
manifest or its package mapping is not an object (TRY004). Nothing
catches these errors: the script still exits with a traceback, so the
bundle-update script fails exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test that builds a raised ValueError now raises it in a helper, so
the raise leaves the try block (TRY301) and the error keeps its
traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reform normalizer's float() coercion now catches only TypeError,
ValueError and OverflowError, the only errors float() raises for the
JSON values a policy holds.

Every other except Exception is a deliberate catch-all and keeps its
behaviour with an inline reason: telemetry, logging and metric calls that
must never fail a request or change a result, cache reads and writes that
degrade to a miss or False, best-effort cleanup, the health probe, the
search route's JSON 500, the committed-write mirror fallbacks, and the
import-time version fallback (BLE001). The health probe also returns its
result from an else block (TRY300).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select flake8-blind-except (BLE), flake8-errmsg (EM) and tryceratops
(TRY). Tests follow all three: their exemptions stay limited to S101,
D1, ANN and PLR2004.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPMValidationError takes a stable error code and then the client-facing
message, so the 18 variables the EM fix named msg are renamed error_code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ten older suppressions, active now that BLE is enabled, used the
"- reason" form; they now match every other suppression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api.quality.ruff-lint now lists the group 5 families, and the rendered
rules page and AGENTS.md Rules block are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3918's _serialize_region_filter_value passes f-string messages straight
to CatalogExtractionError, which EM102 and TRY003, enabled on this
branch, report. Assign each message to msg first, as the rest of the
extraction module does; the messages are unchanged.

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

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.

1 participant