Fix Variable.clone() to preserve update_variable-inherited attributes (#502) - #511
Fix Variable.clone() to preserve update_variable-inherited attributes (#502)#511anth-volk wants to merge 7 commits into
Conversation
Lock the existing clone() contract for normal variables (attributes preserved, same class, independent mutable containers) before fixing #502. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) Cloning a variable registered via a reform's update_variable currently raises "Missing attribute 'value_type'" because clone() re-runs __init__ without the baseline. Documented as strict xfail (label-only and adds-only overrides) until the fix lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#502) clone() re-ran __init__ with baseline_variable=None, dropping every attribute a reform's update_variable merged in from the baseline (value_type, entity, formulas, ...) and raising "Missing attribute 'value_type'". Cloning a reformed tax-benefit system — which the YAML test runner and branch calculations do for every variable — therefore crashed. Clone now mirrors TaxBenefitSystem.clone(): empty_clone + __dict__ copy, preserving the variable's merged state, with formulas/metadata copied so the clone stays independent. The two #502 regression tests are un-xfailed. Fixes #502. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an end-to-end test that clones a whole reformed TaxBenefitSystem (the path the YAML test runner and branch calculations take), plus a test that a runtime-set attribute (metadata) is preserved by clone() and stays independent of the original. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docstring now states that formulas and metadata are copied (adds/subtracts are shared, as before) and that baseline_variable is retained. Add tests for the real reform paths that clone under the hood — get_neutralized_variable and get_annualized_variable of an update_variable variable — plus an Enum variable clone and an assertion documenting adds sharing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No conflicts; brings in #518 (formula-randomness check) and the py3.9/3.10 drop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
left a comment
There was a problem hiding this comment.
Approving. Verified today (2026-09-02) against current master, not the 7/8 CI run:
- Merged this branch onto
master@ 9125de0 locally (clean auto-merge;mastergained #518'scheck_formula_determinismcall inVariable.__init__, which a__dict__-copy clone correctly does not re-run).tests/core: 707 passed, 1 skipped, 1 xfailed. - Ran the real callers of the changed path against the merged core: policyengine-us reform YAML suites that
neutralize_variable(FISC Act, Family Security Act HoH removal, End Child Poverty Act, EDAA, state dependent-exemption repeal) — 28 passed. policyengine-uktest_behavioral_responses.py+test_labour_supply_response_formulas.py(branch →Simulation.clone→TaxBenefitSystem.clone) — 6 passed, 11 skipped. The 6 failures intest_labour_supply_responses.yamlreproduce identically on stock core (dict + listTypeError), so they are pre-existing and unrelated.
Semantics check: empty_clone + __dict__ copy is exactly the TaxBenefitSystem.clone pattern; formulas/metadata copied, adds/subtracts shared as before. One behaviour change worth naming because it is an improvement, not a regression: the old __init__-based clone reset instance state to class defaults, so system.clone() silently dropped is_neutralized=True (and any replaced formulas) from every variable. Clones now preserve runtime state, which is what every caller (get_neutralized_variable, get_annualized_variable, branches, the YAML runner) actually wants.
The CI on this PR is stale (July 8, still on the py3.9/3.10 matrix); a merge of master into the branch refreshes it.
Fixes #502
Summary
Variable.clone()re-ran__init__withbaseline_variable=None, dropping every attribute a reform'supdate_variablemerged in from the baseline (value_type,entity,formulas, …) and raisingValueError: Missing attribute 'value_type'. BecauseTaxBenefitSystem.clone()clones every variable — and the YAML test runner + branch calculations clone the system — any reform usingupdate_variablecrashed on clone.Fix
clone()now mirrorsTaxBenefitSystem.clone():empty_clone+ a__dict__copy, preserving the variable's merged (baseline + override) state instead of re-deriving it.formulas/metadataare copied so the clone stays independent of the original (the prior__init__-based clone produced fresh containers). Not re-running__init__also avoids a spuriouscheck_computation_modes()re-validation.Verified safe for all three
Variable.clone()callers (get_annualised_variable,get_neutralized_variable,TaxBenefitSystem.clone) — they reassign containers or mutate scalars, never mutateformulas/metadatain place. No callers in policyengine-us/uk.Commits (TDD)
clone()(green on unchanged code).xfail(label-only + adds-only overrides).TaxBenefitSystem(the test-runner/branch path) + a runtime-attribute preservation/independence test.Verification
tests/core/variables/test_variable_clone.py: 7 tests.tests/core: 658 passed, 1 skipped, 1 xfailed (no regressions).🤖 Generated with Claude Code