diff --git a/changelog.d/bump-country-pins.fixed.md b/changelog.d/bump-country-pins.fixed.md new file mode 100644 index 00000000..804ecd51 --- /dev/null +++ b/changelog.d/bump-country-pins.fixed.md @@ -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. diff --git a/changelog.d/relax-manifest-check.changed.md b/changelog.d/relax-manifest-check.changed.md new file mode 100644 index 00000000..fd4f34e7 --- /dev/null +++ b/changelog.d/relax-manifest-check.changed.md @@ -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 `>=`. diff --git a/pyproject.toml b/pyproject.toml index bc5034b3..a620f96b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -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", diff --git a/src/policyengine/data/release_manifests/uk.json b/src/policyengine/data/release_manifests/uk.json index 90cc1cc1..1ef3a800 100644 --- a/src/policyengine/data/release_manifests/uk.json +++ b/src/policyengine/data/release_manifests/uk.json @@ -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", @@ -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", diff --git a/src/policyengine/data/release_manifests/us.json b/src/policyengine/data/release_manifests/us.json index 20526da9..f4815645 100644 --- a/src/policyengine/data/release_manifests/us.json +++ b/src/policyengine/data/release_manifests/us.json @@ -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", @@ -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", diff --git a/src/policyengine/tax_benefit_models/uk/model.py b/src/policyengine/tax_benefit_models/uk/model.py index edd5c069..1d6711d0 100644 --- a/src/policyengine/tax_benefit_models/uk/model.py +++ b/src/policyengine/tax_benefit_models/uk/model.py @@ -1,4 +1,5 @@ import datetime +import warnings from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING, Optional @@ -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() diff --git a/src/policyengine/tax_benefit_models/us/model.py b/src/policyengine/tax_benefit_models/us/model.py index a896f5c4..f5aca625 100644 --- a/src/policyengine/tax_benefit_models/us/model.py +++ b/src/policyengine/tax_benefit_models/us/model.py @@ -1,4 +1,5 @@ import datetime +import warnings from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING, Optional @@ -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() diff --git a/tests/test_manifest_version_mismatch.py b/tests/test_manifest_version_mismatch.py new file mode 100644 index 00000000..f9145556 --- /dev/null +++ b/tests/test_manifest_version_mismatch.py @@ -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}" + ) diff --git a/tests/test_models.py b/tests/test_models.py index 146e8532..fbc613d2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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 ( @@ -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 ( diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 35c6e17b..c2370546 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -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" @@ -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") @@ -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 ( @@ -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") @@ -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 @@ -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"