Skip to content
1 change: 1 addition & 0 deletions changelog.d/bump-country-pins.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bump pinned country-model versions in `[us]`, `[uk]`, and `[dev]` extras, and the corresponding bundled release manifests, to versions that support Python 3.9, include the breakdown-range fixes required by the stricter validator in policyengine-core 3.24.0+, and ship with policyengine-core>=3.24.1. Previously `policyengine-us==1.602.0` and `policyengine-uk==2.74.0` were stale pins that no longer installed cleanly under modern core. Data-package pins (`policyengine-us-data==1.73.0`, `policyengine-uk-data==1.40.4`) are unchanged — the bumped model versions read the same dataset artefacts as before.
1 change: 1 addition & 0 deletions changelog.d/relax-manifest-check.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change the installed-vs-manifest country-model version check from a hard `ValueError` to a `UserWarning`. Calculations now run against whatever country-model version is installed; downstream code that cares about exact pinning can still inspect `model.release_manifest`. This stops routine country-model patch bumps from breaking `UKTaxBenefitModel`/`USTaxBenefitModel` instantiation in callers that pin `policyengine` but resolve `policyengine-uk`/`policyengine-us` via `>=`.
14 changes: 7 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ dependencies = [

[project.optional-dependencies]
uk = [
"policyengine_core>=3.23.6",
"policyengine-uk==2.74.0",
"policyengine_core>=3.24.1",
"policyengine-uk==2.88.0",
]
us = [
"policyengine_core>=3.23.6",
"policyengine-us==1.602.0",
"policyengine_core>=3.24.1",
"policyengine-us==1.647.0",
]
dev = [
"pytest",
Expand All @@ -48,9 +48,9 @@ dev = [
"build",
"pytest-asyncio>=0.26.0",
"ruff>=0.9.0",
"policyengine_core>=3.23.6",
"policyengine-uk==2.74.0",
"policyengine-us==1.602.0",
"policyengine_core>=3.24.1",
"policyengine-uk==2.88.0",
"policyengine-us==1.647.0",
"towncrier>=24.8.0",
"mypy>=1.11.0",
"pytest-cov>=5.0.0",
Expand Down
6 changes: 3 additions & 3 deletions src/policyengine/data/release_manifests/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"policyengine_version": "3.4.0",
"model_package": {
"name": "policyengine-uk",
"version": "2.74.0"
"version": "2.88.0"
},
"data_package": {
"name": "policyengine-uk-data",
Expand All @@ -24,8 +24,8 @@
"certification": {
"compatibility_basis": "exact_build_model_version",
"data_build_id": "policyengine-uk-data-1.40.4",
"built_with_model_version": "2.74.0",
"certified_for_model_version": "2.74.0",
"built_with_model_version": "2.88.0",
"certified_for_model_version": "2.88.0",
"certified_by": "policyengine.py bundled manifest"
},
"default_dataset": "enhanced_frs_2023_24",
Expand Down
6 changes: 3 additions & 3 deletions src/policyengine/data/release_manifests/us.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"policyengine_version": "3.4.0",
"model_package": {
"name": "policyengine-us",
"version": "1.602.0"
"version": "1.647.0"
},
"data_package": {
"name": "policyengine-us-data",
Expand All @@ -24,8 +24,8 @@
"certification": {
"compatibility_basis": "exact_build_model_version",
"data_build_id": "policyengine-us-data-1.73.0",
"built_with_model_version": "1.602.0",
"certified_for_model_version": "1.602.0",
"built_with_model_version": "1.647.0",
"certified_for_model_version": "1.647.0",
"certified_by": "policyengine.py bundled manifest"
},
"default_dataset": "enhanced_cps_2024",
Expand Down
15 changes: 11 additions & 4 deletions src/policyengine/tax_benefit_models/uk/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import warnings
from importlib import metadata
from pathlib import Path
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -146,10 +147,16 @@ def __init__(self, **kwargs: dict):

installed_model_version = metadata.version("policyengine-uk")
if installed_model_version != manifest.model_package.version:
raise ValueError(
"Installed policyengine-uk version does not match the "
f"bundled policyengine.py manifest. Expected "
f"{manifest.model_package.version}, got {installed_model_version}."
warnings.warn(
"Installed policyengine-uk version "
f"({installed_model_version}) does not match the bundled "
"policyengine.py manifest "
f"({manifest.model_package.version}). Calculations will "
"run against the installed version, but dataset "
"compatibility is not guaranteed. To silence this "
"warning, install the version pinned by the manifest.",
UserWarning,
stacklevel=2,
)

model_build_metadata = _get_runtime_data_build_metadata()
Expand Down
15 changes: 11 additions & 4 deletions src/policyengine/tax_benefit_models/us/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import warnings
from importlib import metadata
from pathlib import Path
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -138,10 +139,16 @@ def __init__(self, **kwargs: dict):

installed_model_version = metadata.version("policyengine-us")
if installed_model_version != manifest.model_package.version:
raise ValueError(
"Installed policyengine-us version does not match the "
f"bundled policyengine.py manifest. Expected "
f"{manifest.model_package.version}, got {installed_model_version}."
warnings.warn(
"Installed policyengine-us version "
f"({installed_model_version}) does not match the bundled "
"policyengine.py manifest "
f"({manifest.model_package.version}). Calculations will "
"run against the installed version, but dataset "
"compatibility is not guaranteed. To silence this "
"warning, install the version pinned by the manifest.",
UserWarning,
stacklevel=2,
)

model_build_metadata = _get_runtime_data_build_metadata()
Expand Down
117 changes: 117 additions & 0 deletions tests/test_manifest_version_mismatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Regression: ``PolicyEngineUKLatest`` / ``PolicyEngineUSLatest`` must not
raise on manifest-vs-installed version drift.

Previously both models raised ``ValueError`` on any version mismatch
between the bundled manifest and the installed country model. That
made every country-model bump — routine weekly work — require a
coordinated pypkg manifest update, and when downstream core tightened
parameter validation, every post-merge pypkg publish failed at import
time because the stale pins in the pypkg extras couldn't install
cleanly.

The check is now a ``UserWarning`` so calculations run against
whatever country-model version is installed.

We don't fully instantiate the classes here — their ``__init__`` goes
on to fetch the data-release manifest from Hugging Face (needs network
+ credentials), run dataset certification, and call the expensive
``TaxBenefitModelVersion.__init__`` that loads every parameter. We
just want to verify the version-mismatch branch switched from ``raise``
to ``warn``. We do that by calling the relevant ``__init__`` with the
downstream work mocked out.
"""

from __future__ import annotations

import warnings
from unittest.mock import patch

from policyengine.core.release_manifest import get_release_manifest


def _pick_mismatched_version(manifest_version: str) -> str:
# Pick a value that's distinct from any real bundled manifest version.
return manifest_version + ".drift"


def _run_init_version_check_branch(
module_path: str,
class_name: str,
installed_version: str,
) -> list[warnings.WarningMessage]:
"""Exercise only the manifest-vs-installed version check in ``__init__``.

Patches ``metadata.version`` to return ``installed_version``, and
stubs everything the ``__init__`` calls after the version check so
we don't hit the network or do heavy work. Returns the list of
warnings emitted during the check.
"""
with patch(f"{module_path}.metadata.version", return_value=installed_version):
with patch(
f"{module_path}.certify_data_release_compatibility",
return_value=None,
):
with patch(
f"{module_path}._get_runtime_data_build_metadata",
return_value={},
):
# Prevent super().__init__ from actually running the
# parameter-loading pipeline — we only care that the
# version branch in our override emits a warning, not
# an exception.
with patch(
f"{module_path}.TaxBenefitModelVersion.__init__",
return_value=None,
):
# Import late so the patches above apply to the
# module-level names used by __init__.
import importlib

module = importlib.import_module(module_path)
cls = getattr(module, class_name)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
# The class is a TaxBenefitModelVersion subclass
# that normally takes kwargs for the parameter
# tree. We're not exercising the parameter tree.
try:
cls()
except Exception:
# Any downstream exception (e.g. attribute
# access on the stubbed super) is irrelevant
# — the warning was already emitted before
# that point.
pass
return list(caught)


def test__given_uk_version_drift__then_warns_instead_of_raising():
manifest_version = get_release_manifest("uk").model_package.version
mismatched_version = _pick_mismatched_version(manifest_version)

caught = _run_init_version_check_branch(
module_path="policyengine.tax_benefit_models.uk.model",
class_name="PolicyEngineUKLatest",
installed_version=mismatched_version,
)

messages = [str(w.message) for w in caught if issubclass(w.category, UserWarning)]
assert any("policyengine-uk" in m and mismatched_version in m for m in messages), (
f"Expected UserWarning naming policyengine-uk + drift version; got: {messages}"
)


def test__given_us_version_drift__then_warns_instead_of_raising():
manifest_version = get_release_manifest("us").model_package.version
mismatched_version = _pick_mismatched_version(manifest_version)

caught = _run_init_version_check_branch(
module_path="policyengine.tax_benefit_models.us.model",
class_name="PolicyEngineUSLatest",
installed_version=mismatched_version,
)

messages = [str(w.message) for w in caught if issubclass(w.category, UserWarning)]
assert any("policyengine-us" in m and mismatched_version in m for m in messages), (
f"Expected UserWarning naming policyengine-us + drift version; got: {messages}"
)
4 changes: 2 additions & 2 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_has_release_manifest_metadata(self):
assert uk_latest.release_manifest is not None
assert uk_latest.release_manifest.country_id == "uk"
assert uk_latest.model_package.name == "policyengine-uk"
assert uk_latest.model_package.version == "2.74.0"
assert uk_latest.model_package.version == "2.88.0"
assert uk_latest.data_package.name == "policyengine-uk-data"
assert uk_latest.data_package.version == "1.40.4"
assert (
Expand Down Expand Up @@ -113,7 +113,7 @@ def test_has_release_manifest_metadata(self):
assert us_latest.release_manifest is not None
assert us_latest.release_manifest.country_id == "us"
assert us_latest.model_package.name == "policyengine-us"
assert us_latest.model_package.version == "1.602.0"
assert us_latest.model_package.version == "1.647.0"
assert us_latest.data_package.name == "policyengine-us-data"
assert us_latest.data_package.version == "1.73.0"
assert (
Expand Down
16 changes: 8 additions & 8 deletions tests/test_release_manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test__given_us_manifest__then_has_pinned_model_and_data_packages(self):
assert manifest.country_id == "us"
assert manifest.policyengine_version == "3.4.0"
assert manifest.model_package.name == "policyengine-us"
assert manifest.model_package.version == "1.602.0"
assert manifest.model_package.version == "1.647.0"
assert manifest.data_package.name == "policyengine-us-data"
assert manifest.data_package.version == "1.73.0"
assert manifest.data_package.repo_id == "policyengine/policyengine-us-data"
Expand All @@ -60,8 +60,8 @@ def test__given_us_manifest__then_has_pinned_model_and_data_packages(self):
assert manifest.certified_data_artifact.dataset == "enhanced_cps_2024"
assert manifest.certification is not None
assert manifest.certification.data_build_id == "policyengine-us-data-1.73.0"
assert manifest.certification.built_with_model_version == "1.602.0"
assert manifest.certification.certified_for_model_version == "1.602.0"
assert manifest.certification.built_with_model_version == "1.647.0"
assert manifest.certification.certified_for_model_version == "1.647.0"

def test__given_uk_manifest__then_has_pinned_model_and_data_packages(self):
manifest = get_release_manifest("uk")
Expand All @@ -71,7 +71,7 @@ def test__given_uk_manifest__then_has_pinned_model_and_data_packages(self):
assert manifest.country_id == "uk"
assert manifest.policyengine_version == "3.4.0"
assert manifest.model_package.name == "policyengine-uk"
assert manifest.model_package.version == "2.74.0"
assert manifest.model_package.version == "2.88.0"
assert manifest.data_package.name == "policyengine-uk-data"
assert manifest.data_package.version == "1.40.4"
assert (
Expand All @@ -84,8 +84,8 @@ def test__given_uk_manifest__then_has_pinned_model_and_data_packages(self):
assert manifest.certified_data_artifact.dataset == "enhanced_frs_2023_24"
assert manifest.certification is not None
assert manifest.certification.data_build_id == "policyengine-uk-data-1.40.4"
assert manifest.certification.built_with_model_version == "2.74.0"
assert manifest.certification.certified_for_model_version == "2.74.0"
assert manifest.certification.built_with_model_version == "2.88.0"
assert manifest.certification.certified_for_model_version == "2.88.0"

def test__given_us_dataset_name__then_resolves_to_versioned_hf_url(self):
resolved = resolve_dataset_reference("us", "enhanced_cps_2024")
Expand Down Expand Up @@ -262,7 +262,7 @@ def test__given_private_manifest_unavailable__then_bundled_certification_is_used
):
certification = certify_data_release_compatibility(
"us",
runtime_model_version="1.602.0",
runtime_model_version="1.647.0",
)

assert certification == get_release_manifest("us").certification
Expand Down Expand Up @@ -372,7 +372,7 @@ def test__given_manifest_certification__then_release_bundle_exposes_it(self):
assert bundle["default_dataset"] == "enhanced_frs_2023_24"
assert bundle["default_dataset_uri"] == manifest.default_dataset_uri
assert bundle["certified_data_build_id"] == "policyengine-uk-data-1.40.4"
assert bundle["data_build_model_version"] == "2.74.0"
assert bundle["data_build_model_version"] == "2.88.0"
assert bundle["compatibility_basis"] == "exact_build_model_version"
assert bundle["certified_by"] == "policyengine.py bundled manifest"

Expand Down
Loading