From 428d8ce5ac7ee25c489b993d8bf6f482ab84bfd4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 22:05:22 -0500 Subject: [PATCH 1/3] Point uv.lock at policyengine-core#653 and update the neutralised CGT test From policyengine-core 3.34.8, applying a reform that neutralises a variable gives the variable's existing holder the neutralised definition. Holder.set_input then applies the rule that TaxBenefitSystem.neutralize_variable documents: setting an input for a neutralised variable has no effect except a warning. Up to 3.34.1, a holder that core created before the neutralisation kept the old definition and stored such an input. On the neutralize_variable route, test_neutralized_supplied_gains_report_default_without_unused_warning set 555 after the neutralising reform and read it back after the restoring reform. Core now drops that 555, so the test expects the situation's 120,000. Until core publishes 3.34.9, uv.lock installs core from #653's head (14837f78), so CI tests that fix before its release. Core's dependencies at 14837f78 match 3.34.1's, so no other locked package moves. Co-Authored-By: Claude Opus 5.5 --- changelog.d/core-3349.changed.md | 1 + policyengine_uk/tests/test_capital_gains_projection.py | 10 +++++++++- pyproject.toml | 5 +++++ uv.lock | 10 +++------- 4 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 changelog.d/core-3349.changed.md diff --git a/changelog.d/core-3349.changed.md b/changelog.d/core-3349.changed.md new file mode 100644 index 0000000000..02f022f02f --- /dev/null +++ b/changelog.d/core-3349.changed.md @@ -0,0 +1 @@ +Require policyengine-core 3.34.9 or later. From that release, an input set on a variable that a reform has neutralised has no effect, as core documents, so a reform that restores the variable reads the input supplied before neutralisation. diff --git a/policyengine_uk/tests/test_capital_gains_projection.py b/policyengine_uk/tests/test_capital_gains_projection.py index 568131b074..680ccf92ed 100644 --- a/policyengine_uk/tests/test_capital_gains_projection.py +++ b/policyengine_uk/tests/test_capital_gains_projection.py @@ -1068,8 +1068,16 @@ def apply(self): simulation.apply_reform( {"gov.abolitions.capital_gains_before_response": {"2026": False}} ) + # Core ignores inputs to a neutralized variable + # (TaxBenefitSystem.neutralize_variable), so it drops the 555 set under + # NeutralizeGains and the restored variable reads the situation's 120,000. + # The abolition parameter leaves the variable's definition in place, so + # core stores that 555. + expected = 120_000 if route == "neutralize_variable" else 555 with pytest.warns(UserWarning, match="supplied"): - assert simulation.calculate("capital_gains_before_response", 2026)[0] == 555 + assert ( + simulation.calculate("capital_gains_before_response", 2026)[0] == expected + ) restored_record = simulation.capital_gains_projection_metadata["application"][ "by_variable" ]["capital_gains_before_response"]["2026"] diff --git a/pyproject.toml b/pyproject.toml index 6ab0758898..33a3d6697e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,11 @@ dev = [ "rich", ] +[tool.uv.sources] +# Draft only: policyengine-core#653 before core releases it as 3.34.9. +# The PyPI release replaces this before the pull request leaves draft. +policyengine-core = { git = "https://github.com/PolicyEngine/policyengine-core", rev = "14837f784b52d731101dd2e322c7727895d08753" } + [tool.pytest.ini_options] markers = [ "microsimulation: marks tests that use full microsimulation (deselect with '-m \"not microsimulation\"')", diff --git a/uv.lock b/uv.lock index 4b42fb850d..16f9a2a336 100644 --- a/uv.lock +++ b/uv.lock @@ -2195,8 +2195,8 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.34.1" -source = { registry = "https://pypi.org/simple" } +version = "3.34.8" +source = { git = "https://github.com/PolicyEngine/policyengine-core?rev=14837f784b52d731101dd2e322c7727895d08753#14837f784b52d731101dd2e322c7727895d08753" } dependencies = [ { name = "dpath" }, { name = "h5py" }, @@ -2216,10 +2216,6 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/a0/854c180bca2ae6eb44a09da0d6a8344269945d8bf2c39b4b9e46189bbdb7/policyengine_core-3.34.1.tar.gz", hash = "sha256:da4073b3ff63088fa6527c748ba7ebaf8d8c2c624be115d838cabe1070bcd320", size = 740314, upload-time = "2026-10-31T03:45:34.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/64/ffcefacf51632497aa1dfbf6149b946d80ecc2d206175b548875ed8226a2/policyengine_core-3.34.1-py3-none-any.whl", hash = "sha256:bba1af3b34615ae9abbc9c2c1e649fa742e33d85a47e63cf475fa230b4cdd93a", size = 306228, upload-time = "2026-10-31T03:45:32.761Z" }, -] [[package]] name = "policyengine-uk" @@ -2259,7 +2255,7 @@ requires-dist = [ { name = "hypothesis", marker = "extra == 'dev'" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0a0" }, { name = "microdf-python", specifier = ">=1.2.1" }, - { name = "policyengine-core", specifier = ">=3.33.0" }, + { name = "policyengine-core", git = "https://github.com/PolicyEngine/policyengine-core?rev=14837f784b52d731101dd2e322c7727895d08753" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "pytest-xdist", marker = "extra == 'dev'" }, From 58b342e553cc5d4bc260d89fc4c08746d91b2192 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 07:28:19 -0500 Subject: [PATCH 2/3] Require policyengine-core 3.34.9 from PyPI Core published policyengine-core#653 as 3.34.9. Lock 3.34.9's wheel and sdist from PyPI in place of #653's head (14837f78) from GitHub. Drop that git source from pyproject.toml and require policyengine-core>=3.34.9. Core's release commit changes only the version and the changelog, and the 3.34.9 wheel's 194 package files match 14837f78 byte for byte. The release's dependencies match 3.34.1's, so no other locked package moves. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 7 +------ uv.lock | 10 +++++++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33a3d6697e..b0938a166f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] requires-python = ">=3.11" dependencies = [ - "policyengine-core>=3.33.0", + "policyengine-core>=3.34.9", "microdf-python>=1.2.1", "pydantic>=2.11.7", "tables>=3.10.2", @@ -80,11 +80,6 @@ dev = [ "rich", ] -[tool.uv.sources] -# Draft only: policyengine-core#653 before core releases it as 3.34.9. -# The PyPI release replaces this before the pull request leaves draft. -policyengine-core = { git = "https://github.com/PolicyEngine/policyengine-core", rev = "14837f784b52d731101dd2e322c7727895d08753" } - [tool.pytest.ini_options] markers = [ "microsimulation: marks tests that use full microsimulation (deselect with '-m \"not microsimulation\"')", diff --git a/uv.lock b/uv.lock index 16f9a2a336..9e30ce16a9 100644 --- a/uv.lock +++ b/uv.lock @@ -2195,8 +2195,8 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.34.8" -source = { git = "https://github.com/PolicyEngine/policyengine-core?rev=14837f784b52d731101dd2e322c7727895d08753#14837f784b52d731101dd2e322c7727895d08753" } +version = "3.34.9" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, { name = "h5py" }, @@ -2216,6 +2216,10 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/a3/28/f95c5723c7840bd918f14f9a304466c8fae550529b3ce58033611389419f/policyengine_core-3.34.9.tar.gz", hash = "sha256:685334fed586e886527a995f7c622cb41a48548c49c780655f531fc3236b9639", size = 760350, upload-time = "2026-11-07T12:24:12.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5a/b250004db2ec2462fceb612c60c566070598d63e154316ed2c66fdfe19af/policyengine_core-3.34.9-py3-none-any.whl", hash = "sha256:3b100d536375958abfff367aef4c03d30064daf0c77bcdc270d1dc4268215406", size = 308111, upload-time = "2026-11-07T12:24:11.114Z" }, +] [[package]] name = "policyengine-uk" @@ -2255,7 +2259,7 @@ requires-dist = [ { name = "hypothesis", marker = "extra == 'dev'" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0a0" }, { name = "microdf-python", specifier = ">=1.2.1" }, - { name = "policyengine-core", git = "https://github.com/PolicyEngine/policyengine-core?rev=14837f784b52d731101dd2e322c7727895d08753" }, + { name = "policyengine-core", specifier = ">=3.34.9" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "pytest-xdist", marker = "extra == 'dev'" }, From 7e7ad197e24765da7c3ce181b0d9fecb0f9d5595 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 08:35:56 -0500 Subject: [PATCH 3/3] Unmark the restoration cases now that core 3.34.9 resyncs holders From 3.34.9, Population.get_holder gives an existing holder its variable's current definition whenever the holder's storage fits that definition. After a reform restores a neutralised variable, a write to that variable now replaces the value its holder kept through the neutralisation. Drop the strict expected failures from both variants of test_neutralized_inputs_and_model_values_survive_subsampling and from the subsampled variants of test_restoring_reform_can_replace_a_neutralized_input, with their two allowlist entries. Co-Authored-By: Claude Opus 5.5 --- .github/test_outcome_allowlist.toml | 12 ------------ policyengine_uk/tests/test_input_records.py | 13 +------------ policyengine_uk/tests/test_subsample.py | 5 ----- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/.github/test_outcome_allowlist.toml b/.github/test_outcome_allowlist.toml index 4689a1449e..a1bdda7f7f 100644 --- a/.github/test_outcome_allowlist.toml +++ b/.github/test_outcome_allowlist.toml @@ -19,18 +19,6 @@ # Keep entries narrow. An entry that matches nothing in a run is reported as a # warning; remove it once its bug is fixed. Failures can never be accepted. -[[allow]] -nodeid = "policyengine_uk/tests/test_subsample.py::test_neutralized_inputs_and_model_values_survive_subsampling[[]*]" -outcome = "xfailed" -reason = "^needs policyengine-core holder resync$" -why = "Issue #2254 / PR #2265 waits for core to resync restored holders after subsampling. Both ownership variants retain every assertion under strict xfail." - -[[allow]] -nodeid = "policyengine_uk/tests/test_input_records.py::test_restoring_reform_can_replace_a_neutralized_input[[]subsampled-*]" -outcome = "xfailed" -reason = "^needs policyengine-core holder resync$" -why = "Issue #2254 / PR #2265 waits for core to accept writes immediately after restoring a subsampled holder. Direct supplied/model controls remain unmarked." - [[allow]] nodeid = "policyengine_uk/tests/test_uc_state_pension_properties.py::test_state_pension_does_not_change_partners_marriage_allowance" outcome = "xfailed" diff --git a/policyengine_uk/tests/test_input_records.py b/policyengine_uk/tests/test_input_records.py index 24b795e4fd..5ab9a3941d 100644 --- a/policyengine_uk/tests/test_input_records.py +++ b/policyengine_uk/tests/test_input_records.py @@ -189,18 +189,7 @@ def test_rebuilt_populations_forget_the_inputs_they_replace(): @pytest.mark.parametrize("model", [False, True], ids=["supplied", "model"]) @pytest.mark.parametrize( "subsampled", - [ - pytest.param(False, id="direct"), - pytest.param( - True, - id="subsampled", - marks=pytest.mark.xfail( - strict=True, - raises=AssertionError, - reason="needs policyengine-core holder resync", - ), - ), - ], + [pytest.param(False, id="direct"), pytest.param(True, id="subsampled")], ) def test_restoring_reform_can_replace_a_neutralized_input(model, subsampled): """A write after restoration replaces the hidden value and its ownership.""" diff --git a/policyengine_uk/tests/test_subsample.py b/policyengine_uk/tests/test_subsample.py index 2417c08aed..6f3cc5b463 100644 --- a/policyengine_uk/tests/test_subsample.py +++ b/policyengine_uk/tests/test_subsample.py @@ -526,11 +526,6 @@ def test_a_model_value_stays_the_models(): @pytest.mark.parametrize("model", [False, True], ids=["supplied", "model"]) -@pytest.mark.xfail( - strict=True, - raises=AssertionError, - reason="needs policyengine-core holder resync", -) def test_neutralized_inputs_and_model_values_survive_subsampling(model): """Neutralisation hides stored inputs without discarding them or their owner.""" data = dataset([(100.0, [1]), (100.0, [1])])