Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-uprating-end-year-guard.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise `UpratingYearOutOfRangeError` with a clear message when `uprate_values` or `uprate_dataset` is called with a year outside the `[START_YEAR, END_YEAR]` range of the uprating factor table, instead of surfacing a pandas `KeyError` or silently returning wrong values.
88 changes: 88 additions & 0 deletions policyengine_uk_data/tests/test_uprating_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Tests for year-range validation in `policyengine_uk_data.utils.uprating`.

The uprating factor table covers ``[START_YEAR, END_YEAR]`` (2020–2034 on
main). Callers that request a year outside this range previously got a
raw pandas ``KeyError`` or a silently wrong value. After the fix in
finding U5 they must get ``UpratingYearOutOfRangeError`` with a clear
message.
"""

from __future__ import annotations

import importlib.util

import pandas as pd
import pytest

if importlib.util.find_spec("policyengine_uk") is None:
pytest.skip(
"policyengine_uk not available in test environment",
allow_module_level=True,
)


def _seed_uprating_table(tmp_path, start=2020, end=2034):
"""Write a minimal uprating_factors.csv covering a known year range."""

storage = tmp_path / "storage"
storage.mkdir()
years = list(range(start, end + 1))
table = pd.DataFrame(
{str(y): [1.0 + 0.02 * (y - start)] for y in years},
index=pd.Index(["employment_income"], name="Variable"),
)
table.to_csv(storage / "uprating_factors.csv")
return storage


def test_uprate_dataset_rejects_year_above_end(tmp_path, monkeypatch):
from policyengine_uk_data.utils import uprating as uprating_module
from policyengine_uk_data.utils.uprating import (
UpratingYearOutOfRangeError,
uprate_dataset,
)

storage = _seed_uprating_table(tmp_path)
monkeypatch.setattr(uprating_module, "STORAGE_FOLDER", storage)

# target_year=2035 is above END_YEAR=2034 → must raise with clear message.
class _Dataset:
time_period = 2023
tables = []

def copy(self):
return self

with pytest.raises(UpratingYearOutOfRangeError, match="target_year=2035"):
uprate_dataset(_Dataset(), target_year=2035)


def test_uprate_values_rejects_years_outside_range(tmp_path, monkeypatch):
from policyengine_uk_data.utils import uprating as uprating_module
from policyengine_uk_data.utils.uprating import (
UpratingYearOutOfRangeError,
uprate_values,
)

storage = _seed_uprating_table(tmp_path)
monkeypatch.setattr(uprating_module, "STORAGE_FOLDER", storage)

with pytest.raises(UpratingYearOutOfRangeError, match="end_year=2099"):
uprate_values(100.0, "employment_income", start_year=2020, end_year=2099)

with pytest.raises(UpratingYearOutOfRangeError, match="start_year=1999"):
uprate_values(100.0, "employment_income", start_year=1999, end_year=2034)


def test_uprate_values_accepts_supported_range(tmp_path, monkeypatch):
from policyengine_uk_data.utils import uprating as uprating_module
from policyengine_uk_data.utils.uprating import uprate_values

storage = _seed_uprating_table(tmp_path)
monkeypatch.setattr(uprating_module, "STORAGE_FOLDER", storage)

# A supported year range still works and returns a sensible factor.
result = uprate_values(100.0, "employment_income", start_year=2020, end_year=2034)
# Seed table has linear growth 0.02 per year; 2020→2034 = 14 years ×
# 0.02 = 1.28 multiplier off a 1.0 base.
assert result == pytest.approx(128.0)
32 changes: 30 additions & 2 deletions policyengine_uk_data/utils/uprating.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@
END_YEAR = 2034


class UpratingYearOutOfRangeError(ValueError):
"""Raised when a caller asks for an uprating factor outside the table range.

The uprating factor table is written by `create_policyengine_uprating_factors_table()`
for years in ``[START_YEAR, END_YEAR]`` and does not include columns for years
beyond ``END_YEAR``. Silently returning a KeyError (or a stale last-year factor)
would produce wrong values; raising a specific, actionable error instead tells
the caller to either regenerate the table with a later ``END_YEAR`` or pick a
supported target year.
"""


def _check_year_in_range(year: int, *, kind: str) -> None:
if year < START_YEAR or year > END_YEAR:
raise UpratingYearOutOfRangeError(
f"{kind}={year} is outside the uprating table range "
f"[{START_YEAR}, {END_YEAR}]. Regenerate the table via "
f"`create_policyengine_uprating_factors_table()` with a later "
f"END_YEAR, or pick a supported year."
)


def create_policyengine_uprating_factors_table():
from policyengine_uk.system import system

Expand Down Expand Up @@ -45,7 +67,10 @@ def create_policyengine_uprating_factors_table():
return df


def uprate_values(values, variable_name, start_year=2020, end_year=2034):
def uprate_values(values, variable_name, start_year=START_YEAR, end_year=END_YEAR):
_check_year_in_range(start_year, kind="start_year")
_check_year_in_range(end_year, kind="end_year")

uprating_factors = pd.read_csv(STORAGE_FOLDER / "uprating_factors.csv")
uprating_factors = uprating_factors.set_index("Variable")
uprating_factors = uprating_factors.loc[variable_name]
Expand All @@ -57,11 +82,14 @@ def uprate_values(values, variable_name, start_year=2020, end_year=2034):
return values * relative_change


def uprate_dataset(dataset: UKSingleYearDataset, target_year=2034):
def uprate_dataset(dataset: UKSingleYearDataset, target_year: int = END_YEAR):
_check_year_in_range(target_year, kind="target_year")

dataset = dataset.copy()
uprating_factors = pd.read_csv(STORAGE_FOLDER / "uprating_factors.csv")
uprating_factors = uprating_factors.set_index("Variable")
start_year = dataset.time_period
_check_year_in_range(int(start_year), kind="dataset.time_period")

for table in dataset.tables:
for variable in table.columns:
Expand Down
Loading