From 7c78178210960607ef24a8b64ecc39d6c54293bb Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:38:35 +0400 Subject: [PATCH] Reject misplaced uprating rounding metadata --- ...eject-misplaced-uprating-rounding.fixed.md | 1 + policyengine_core/parameters/helpers.py | 17 ++++++ .../parameters/operations/test_uprating.py | 56 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 changelog.d/reject-misplaced-uprating-rounding.fixed.md diff --git a/changelog.d/reject-misplaced-uprating-rounding.fixed.md b/changelog.d/reject-misplaced-uprating-rounding.fixed.md new file mode 100644 index 00000000..6fbae3f5 --- /dev/null +++ b/changelog.d/reject-misplaced-uprating-rounding.fixed.md @@ -0,0 +1 @@ +Parameter loading now rejects uprating rounding rules placed where they would be silently ignored. diff --git a/policyengine_core/parameters/helpers.py b/policyengine_core/parameters/helpers.py index 8435bf46..ab0af7b6 100644 --- a/policyengine_core/parameters/helpers.py +++ b/policyengine_core/parameters/helpers.py @@ -90,10 +90,27 @@ def _validate_parameter(parameter, data, data_type=None, allowed_keys=None): parameter.file_path, ) + if isinstance(data, dict): + _validate_uprating_rounding_metadata(parameter, data.get("metadata")) + if allowed_keys is not None and isinstance(data, dict): _warn_on_unknown_keys(parameter, data, allowed_keys) +def _validate_uprating_rounding_metadata(parameter, metadata): + if not isinstance(metadata, dict): + return + if "uprating" not in metadata or "rounding" not in metadata: + return + + raise ParameterParsingError( + "Invalid uprating metadata for '{}': `rounding` must be nested inside " + "`uprating`; placing both keys at the same level causes the rounding " + "rule to be ignored.".format(parameter.name), + parameter.file_path, + ) + + def _warn_on_unknown_keys(parameter, data, allowed_keys): """Warn about keys placed alongside the recognized parameter keys. diff --git a/tests/core/parameters/operations/test_uprating.py b/tests/core/parameters/operations/test_uprating.py index dcd4eaf7..74aee22e 100644 --- a/tests/core/parameters/operations/test_uprating.py +++ b/tests/core/parameters/operations/test_uprating.py @@ -344,6 +344,62 @@ def test_parameter_uprating_with_rounding(): assert interpolated.to_be_uprated("2018-01-01") == 4 +@pytest.mark.parametrize( + "data", + [ + { + "parameter": { + "values": {"2025-01-01": 1}, + "metadata": { + "uprating": "uprater", + "rounding": {"interval": 1, "type": "upwards"}, + }, + }, + }, + { + "scale": { + "metadata": { + "uprating": "uprater", + "rounding": {"interval": 1, "type": "upwards"}, + }, + "brackets": [ + { + "threshold": {"2025-01-01": 0}, + "rate": {"2025-01-01": 0.1}, + }, + ], + }, + }, + { + "scale": { + "brackets": [ + { + "threshold": {"2025-01-01": 0}, + "rate": {"2025-01-01": 0.1}, + "metadata": { + "uprating": "uprater", + "rounding": { + "interval": 1, + "type": "upwards", + }, + }, + }, + ], + }, + }, + ], +) +def test_parameter_uprating_rejects_rounding_beside_uprating(data): + from policyengine_core.errors import ParameterParsingError + from policyengine_core.parameters import ParameterNode + + with pytest.raises( + ParameterParsingError, + match="`rounding` must be nested inside `uprating`", + ): + ParameterNode(data=data) + + def test_parameter_uprating_with_self(): from policyengine_core.parameters import ParameterNode