Skip to content

Guard the process's warning filters against the model - #2379

Merged
MaxGhenis merged 2 commits into
mainfrom
warning-filters-2377
Oct 12, 2026
Merged

MaxGhenis merged 2 commits into
mainfrom
warning-filters-2377

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2377

Reproducer: policyengine_uk/tests/test_warning_filters.py::test_a_warning_raised_after_simulating_reaches_pytest_warns

axiom: n/a: warning handling

What was wrong

Until 12 October, LHA_allowed_bedrooms.py, LHA_cap.py and LHA_eligible.py called warnings.filterwarnings("ignore") at import, and bi_household_phaseout's formula called it on every run. policyengine-core runs every variable file again each time a CountryTaxBenefitSystem is built (add_variables_from_file → spec.loader.exec_module). So each build of the model put a global "ignore" at the front of warnings.filters. That silenced every later warning in the process, including inside a pytest.warns block entered before the build. On 11 October a ReformPeriodWarning (#2305) never reached the caller.

#1846 (merged 03:07 UTC on 12 October, commit 00c07d6) removed the four filters and replaced the divisions they hid with np.divide(..., where=). Its fresh-interpreter test covers import policyengine_uk. This PR adds the guard that keeps the filters from coming back, and removes #2305's workaround for them.

Where the filters came from (git history)

Filter Added What it hid
LHA modules, at import #787 (2023-11-26), in LHA.py time_shift_dataset assigned columns on filtered pandas slices (df.year = ..., df.weekly_rent = ...), which raises SettingWithCopyWarning. #1139 (2025-06-06) split LHA.py into one file per variable and copied the header into all of them, including these three, which had no pandas code. 43ef96f (2026-10-01) dropped it from BRMA_LHA_rate, LHA_category and uc_LHA_cap.
bi_household_phaseout, in the formula 999424c (2022-10-31) The 0/0 numpy RuntimeWarning from where(household_bi > 0, capped_deduction / household_bi, 0), which evaluates both branches.

None of these filters is needed now. The guarded divisions raise no warning at all, which is better than a scoped catch_warnings(), and no LHA module uses pandas.

Changes

  • policyengine_uk/tests/test_warning_filters.py (new) holds the regression tests below.
  • policyengine_uk/utils/dated_reforms.py: _warn no longer logs each message as well as warning ("some variable modules switch warnings off when loaded"). That reason no longer holds, and callers saw each message twice. The unused logging import and logger go with it.
  • policyengine_uk/tests/test_dated_reform_routes.py: the two ReformPeriodWarning tests now assert with pytest.warns(ReformPeriodWarning) instead of caplog. This is the 11 October case end to end: the warning is raised after the simulation loads the variable modules. The no-warning case uses simplefilter("error", ReformPeriodWarning).
  • Removed the unused import warnings left in bi_individual_phaseout.py and dynamics/participation.py.

Invariants (tested)

  1. Building a CountryTaxBenefitSystem or a Simulation(situation=...) and calculating its variables leaves warnings.filters and numpy's error state as they were. Importing the package leaves numpy's error state alone and adds no filter that ignores a built-in warning category everywhere.
  2. A UserWarning raised after building reaches the caller: inside a pytest.warns block entered before the build, and in a fresh interpreter under Python's default filters.
  3. No module in the package changes the warning filters outside warnings.catch_warnings(). None changes numpy's error state with np.seterr; np.errstate() is the scoped form. An AST scan of all package modules (tests excluded) checks this. A filter inside a formula only runs when that formula runs, so building the model alone cannot catch one. The scanner follows import warnings as w and names imported from warnings or numpy under another name. It flags writes to warnings.filters (assignment, slice assignment, del, list methods), not reads. A class body, decorators and defaults run inside a catch_warnings() block, so they count as scoped. The body of a function or generator expression defined there runs later, so it does not. It has 24 positive and negative cases of its own.
  4. The divisions the filters hid (basic income household phase-out, basic and additional State Pension) raise no RuntimeWarning, including at a zero denominator. The baseline has no basic income, so the phase-out divides by zero there, and the test checks that it does. The test runs under np.errstate(all="warn"), so an "ignore" error state left by earlier code cannot let an unguarded 0/0 pass silently.

The in-process checks start from an "always" filter and np.errstate(all="warn"). Without the first, a filter identical to the one at the front would leave the list unchanged. Without the second, the check would inherit numpy state left by an earlier test, because pytest does not reset it. The fresh-process check snapshots before import policyengine_uk, because the import itself builds a system.

Mutation check

I put each bug back on this branch at 68595fc, ran the 34 new and converted tests (including the scanner's own cases), then reverted. The script is ~/reviews/uk-warning-filters-2377/mutate.sh, and the output is in mutation-results-r2.txt in the same folder.

Mutation Tests that fail
M1: warnings.filterwarnings("ignore") at the top of LHA_cap.py (the pre-#1846 state) 8: scan, build, simulate ×2, pytest.warns, fresh process, both ReformPeriodWarning tests
M2: the same call inside bi_household_phaseout's formula 5: scan, simulate ×2, pytest.warns, fresh process. The build-only test passes, because the formula does not run during a build; the scan exists for this case.
M3: the old unguarded capped_deduction / household_bi, with no filter 1: the divisions test (baseline, zero denominator)
M4: np.seterr(all="ignore") at the top of LHA_cap.py 5: scan, build, simulate ×2, fresh process
M5: warnings.filterwarnings("ignore", category=RuntimeWarning) at the top of LHA_cap.py 5: scan, build, simulate ×2, fresh process
M6: M3, with numpy.seterr(all="ignore") run before the tests (in tests/conftest.py, which the scan skips) 1: the divisions test. At c7a9da7, which lacked the np.errstate pin, the review showed this test passed despite the unguarded 0/0.
M7: from warnings import filterwarnings as fw with fw("ignore") inside bi_household_phaseout's formula 5: scan, simulate ×2, pytest.warns, fresh process

Clean branch: 34 passed.

Tests run

  • make format: ruff format and ruff check pass.
  • pytest -n 4 on test_warning_filters.py, test_dated_reform_routes.py, test_dataset_fiscal_year.py, test_household_benefits_properties.py, test_participation_earnings_quintile.py, test_participation_hours_units.py and test_salary_sacrifice_needs_pay_properties.py: 120 passed.
  • policyengine-core test on the basic income YAML tests (reforms/parametric/basic_income, baseline/contrib/ubi_center): 10 passed.
  • I did not run the full suite locally; CI runs it.
  • Independent review: GPT-6.1 Sol at c7a9da7 asked for changes: the missing Reproducer: line, import aliases getting past the scanner, numpy state in the division test, and the changelog wording. All four are addressed in 68595fc. The report is in ~/reviews/uk-warning-filters-2377/review-sol-c7a9da756.md.
  • Re-review: GPT-6.1 Sol approved at 68595fc. The report is in ~/reviews/uk-warning-filters-2377/review-sol-68595fc14.md. It raised three low, non-blocking points, none of which any current package module hits:
    • the scanner misses an assignment alias such as f = warnings.filters; f.insert(...);
    • it flags a name imported as filters from warnings even after that name is rebound to a list;
    • this body said 26 scanner cases when there are 24 (now corrected).

Cost: the new module takes about 25 s on 4 workers, of which about 8.5 s is the fresh interpreter (peak RSS 360 MB).

Notes

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 12, 2026 05:31
Until #1846, three LHA variable modules called
warnings.filterwarnings("ignore") at import, and bi_household_phaseout
did so in its formula. policyengine-core runs every variable file again
on each CountryTaxBenefitSystem build, so building the model silenced
every warning in the process, including inside pytest.warns. #1846
removed the filters; nothing stopped them coming back.

- Add test_warning_filters.py. It checks that building a system or a
  Simulation and calculating its variables leave warnings.filters and
  numpy's error state unchanged. It checks that a UserWarning raised
  afterwards reaches pytest.warns and a fresh process under default
  filters. An AST scan fails on any unscoped filterwarnings,
  simplefilter, resetwarnings, warnings.filters use or np.seterr in the
  package. It also checks that the divisions the filters hid raise no
  RuntimeWarning, including at a zero denominator.
- Drop the dated_reforms._warn logging workaround (#2305), whose stated
  reason was these filters, and assert ReformPeriodWarning with
  pytest.warns instead of caplog.
- Remove the unused warnings imports left in bi_individual_phaseout and
  dynamics/participation.

Fixes #2377

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

Address the independent review of #2379:

- The scanner follows "import warnings as w" and names imported from
  warnings or numpy under another name, flags only writes to
  warnings.filters (not reads), and treats a class body and a function's
  decorators and defaults as running inside a catch_warnings() block,
  and a generator expression's body as running later.
- The division test runs under np.errstate(all="warn"), so a 0/0 cannot
  pass silently under an "ignore" error state left by earlier code.
- The changelog fragment no longer says importing leaves the filters
  unchanged: dependencies add targeted filters at import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 12, 2026 13:50
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Hand-off to the UK hub

  • Head: 68595fc14b78aa3cffe6978496d15880e0492fbc. Ready for review (not a draft), and mergeable.
  • Independent review: GPT-6.1 Sol asked for changes at c7a9da756. All four points are fixed in 68595fc14, and Sol approved at that head. Reports: ~/reviews/uk-warning-filters-2377/review-sol-c7a9da756.md and review-sol-68595fc14.md. The three remaining points are low and non-blocking, and are listed in the PR body.
  • CI at this head:
    • Pass: Changes, Lint, Documentation build, smoke-import on Python 3.11 to 3.14, Test / YAML, and Test / Python shards 1 to 4.
    • Pending: Test / Result (the shard aggregator), and Fix evidence, which re-runs after the body edit. The repo's own checker from main passes on this body and head.
  • Mutation evidence: seven re-introduced bugs, each caught by the tests meant to catch it. Script and output: ~/reviews/uk-warning-filters-2377/mutate.sh and mutation-results-r2.txt.
  • Depends on: nothing. Roll back failed override batches and keep structural-reform nodes across rebuilds #2375 touches the same two dated-reform files with no conflict in either order (git merge-tree, checked by the review).
  • Hold or decision: none. This changes no model output, and the PR body says axiom: n/a: warning handling.
  • Follow-ups owed: none required. Two optional scanner refinements are possible: following assignment aliases of warnings.filters, and tracking rebinding of an imported name. No current module hits either.

@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 12, 2026
@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit d8020c2 Oct 12, 2026
15 of 17 checks passed
@MaxGhenis
MaxGhenis deleted the warning-filters-2377 branch October 12, 2026 21:24
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged by the UK hub (audit). Merged with a merge commit at the tested head 68595fc14 (--match-head-commit, through the main merge queue). Gates:

  • CI: gh pr checks passed at the head.
  • MERGEABLE, not a draft. No review requested changes.
  • Independent review: GPT-6.1 Sol requested changes at c7a9da756; all four points were fixed in 68595fc14. The Subfleet re-review 20261012-064528-uk2379-rereview (hard tier, workdir head 68595fc14) APPROVEs, with three low non-blocking notes listed in the PR body. Reports: ~/reviews/uk-warning-filters-2377/review-sol-*.md.
  • Impact: none on model output (warning handling only). Seven mutations were each caught by their tests (mutation-results-r2.txt).
  • Interaction with rollup B (Land six reviewed pull requests together (rollup B) #2380): both edit dated_reforms.py and test_dated_reform_routes.py. The hub merged this head onto Land six reviewed pull requests together (rollup B) #2380's head and ran test_dated_reform_routes.py and test_warning_filters.py there: 109 passed (-n 4, locked venv, policyengine-core 3.33.2), with F811/F821 clean. The merge was automatic, so the final tree is the same in either landing order. It lands after Land six reviewed pull requests together (rollup B) #2380.

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.

Guard against process-wide warning filters set while building the model

1 participant