From 92a194fca22424eca4d9a6ade0eee3902e7b13e4 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 13 May 2025 15:54:34 +0200 Subject: [PATCH 01/10] Remove temporary test marks made for #328 --- message_ix_models/tests/model/transport/test_report.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/message_ix_models/tests/model/transport/test_report.py b/message_ix_models/tests/model/transport/test_report.py index e801a9d091..927fbda977 100644 --- a/message_ix_models/tests/model/transport/test_report.py +++ b/message_ix_models/tests/model/transport/test_report.py @@ -167,7 +167,7 @@ def test_bare(request, test_context, tmp_path, regions, years): @mark.parametrize( "build", ( - pytest.param(True, marks=make_mark["gh"](328)), # Run .transport.build.main() + True, # Run .transport.build.main() False, # Use data from an Excel export ), ) @@ -202,7 +202,6 @@ def test_simulated(request, test_context, build, regions="R12", years="B"): @build.get_computer.minimum_version @MARK[10] -@make_mark["gh"](328) def test_simulated_iamc( request, tmp_path_factory, test_context, regions="R12", years="B" ) -> None: @@ -252,7 +251,6 @@ def test_simulated_iamc( @build.get_computer.minimum_version @MARK[10] -@make_mark["gh"](328) @mark.usefixtures("quiet_genno") @pytest.mark.parametrize( "plot_name", From 88af1ab46f5362ef00ec56ce10be32b516b346cb Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Wed, 14 May 2025 09:55:07 +0200 Subject: [PATCH 02/10] TEMPORARY Run "transport" workflow on PR branch --- .github/workflows/transport.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/transport.yaml b/.github/workflows/transport.yaml index 5c278d7ed4..a1ba185e58 100644 --- a/.github/workflows/transport.yaml +++ b/.github/workflows/transport.yaml @@ -49,8 +49,8 @@ env: on: # Uncomment these lines for debugging, but leave them commented on 'main' - # pull_request: - # branches: [ main ] + pull_request: + branches: [ main ] # push: # branches: [ main ] schedule: From aa89e3ac418e2d54f5e9a2abf6c59a48af664e25 Mon Sep 17 00:00:00 2001 From: r-aneeque <114144149+r-aneeque@users.noreply.github.com> Date: Wed, 14 May 2025 10:45:03 +0200 Subject: [PATCH 03/10] run policy setup --- .github/workflows/transport.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/transport.yaml b/.github/workflows/transport.yaml index a1ba185e58..1b76dc444c 100644 --- a/.github/workflows/transport.yaml +++ b/.github/workflows/transport.yaml @@ -36,10 +36,6 @@ env: "SSP4 policy", "SSP5", "SSP5 policy", - "EDITS-CA", - "EDITS-HA", - "LED-SSP1", - "LED-SSP2" ] # Currently disabled: From 4ac80262fd69dc0ad10a16a3e20b03534bccf5b1 Mon Sep 17 00:00:00 2001 From: r-aneeque <114144149+r-aneeque@users.noreply.github.com> Date: Wed, 14 May 2025 11:08:21 +0200 Subject: [PATCH 04/10] Temp: run policy setup --- .github/workflows/transport.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/transport.yaml b/.github/workflows/transport.yaml index 1b76dc444c..c70594b397 100644 --- a/.github/workflows/transport.yaml +++ b/.github/workflows/transport.yaml @@ -35,7 +35,7 @@ env: "SSP4", "SSP4 policy", "SSP5", - "SSP5 policy", + "SSP5 policy" ] # Currently disabled: From f098488e5ced10287d5db76595dbf4a2e6c29e60 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 09:22:26 +0200 Subject: [PATCH 05/10] Add .model.Config.relation_global_co2 --- message_ix_models/model/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/message_ix_models/model/config.py b/message_ix_models/model/config.py index 942c800601..1656fd997d 100644 --- a/message_ix_models/model/config.py +++ b/message_ix_models/model/config.py @@ -38,6 +38,13 @@ class Config(ConfigHelper): #: at :doc:`/pkg-data/relation`. relations: str = "A" + #: ID of the relation used to constrain global total CO₂ emissions. A code with this + #: ID **must** be in the code list identified by :attr:`relations`. + #: + #: In :mod:`message_data`, this ID was stored in the module data variable + #: :py:`message_data.projects.engage.runscript_main.RELATION_GLOBAL_CO2`. + relation_global_co2: str = "CO2_Emission_Global_Total" + #: The 'year' codelist (time periods) to use, Must be one of the lists of periods #: described at :doc:`/pkg-data/year`. years: str = "B" From b8db4f9af54b2813423d3b1b3a6e6dde95c3bc4f Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 09:24:49 +0200 Subject: [PATCH 06/10] Adjust .engage.workflow for #350 - Improve from message_ix_models.project.engage. - Reference .model.Config.relation_global_co2 via Context, instead of hard-coded RELATION_GLOBAL_CO2. --- message_ix_models/project/engage/workflow.py | 54 +++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/message_ix_models/project/engage/workflow.py b/message_ix_models/project/engage/workflow.py index 885c6da5ad..b78792fd80 100644 --- a/message_ix_models/project/engage/workflow.py +++ b/message_ix_models/project/engage/workflow.py @@ -16,23 +16,22 @@ from message_ix import Scenario from message_ix_models import Context, ScenarioInfo -from message_ix_models.model.workflow import Config, solve -from message_ix_models.util import broadcast, identify_nodes +from message_ix_models.model import workflow as model_workflow +from message_ix_models.util import HAS_MESSAGE_DATA, broadcast, identify_nodes from message_ix_models.workflow import Workflow -try: - # NB These modules have not been migrated from message_data.projects.engage - from .runscript_main import glb_co2_relation as RELATION_GLOBAL_CO2 - from .scenario_runner import ScenarioRunner -except ImportError: - RELATION_GLOBAL_CO2 = "" +if HAS_MESSAGE_DATA: + # NB This module has not been migrated from message_data.projects.engage. + from message_data.projects.engage.scenario_runner import ScenarioRunner +else: ScenarioRunner = type("ScenarioRunner", (), {}) + log = logging.getLogger(__name__) @dataclass -class PolicyConfig(Config): +class PolicyConfig(model_workflow.Config): """Configuration for the 3-step ENGAGE workflow for climate policy scenarios.""" #: Label of the climate policy scenario, often related to a global carbon budget in @@ -170,7 +169,7 @@ def step_0(context: Context, scenario: Scenario, **kwargs) -> Scenario: These operations must occur no matter which combinations of 1 or more of :func:`step_1`, :func:`step_2`, and/or :func:`step_3` are to be run on `scenario`. """ - from message_data.tools.utilities import ( + from message_ix_models.tools import ( add_AFOLU_CO2_accounting, add_alternative_TCE_accounting, add_CO2_emission_constraint, @@ -183,22 +182,29 @@ def step_0(context: Context, scenario: Scenario, **kwargs) -> Scenario: except ValueError: pass # Solution did not exist - remove_emission_bounds(scenario) + remove_emission_bounds.main(scenario) # Identify the node codelist used by `scenario` (in case it is not set on `context`) context.model.regions = identify_nodes(scenario) - kw = dict(relation_name=RELATION_GLOBAL_CO2, reg=f"{context.model.regions}_GLB") + kw = dict( + relation_name=context.model.relation_global_co2, + reg=f"{context.model.regions}_GLB", + ) # “Step1.3 Make changes required to run the ENGAGE setup” (per .runscript_main) log.info("Add separate FFI and AFOLU CO2 accounting") - add_FFI_CO2_accounting(scenario, **kw) - add_AFOLU_CO2_accounting(scenario, **kw) + add_FFI_CO2_accounting.main(scenario, **kw, constraint_value=None) + add_AFOLU_CO2_accounting.add_AFOLU_CO2_accounting( + scenario, **kw, constraint_value=None + ) log.info("Add alternative TCE accounting") - add_alternative_TCE_accounting(scenario) + add_alternative_TCE_accounting.main(scenario) - add_CO2_emission_constraint(scenario, **kw, constraint_value=0.0, type_rel="lower") + add_CO2_emission_constraint.main( + scenario, **kw, constraint_value=0.0, type_rel="lower" + ) return scenario @@ -226,7 +232,7 @@ def step_1(context: Context, scenario: Scenario, config: PolicyConfig) -> Scenar def step_2(context: Context, scenario: Scenario, config: PolicyConfig) -> Scenario: """Step 2 of the ENGAGE climate policy workflow.""" - from message_data.tools.utilities import add_emission_trajectory + from message_ix_models.tools import add_emission_trajectory # Retrieve a pandas.DataFrame with the CO2 emissions trajectory # @@ -245,7 +251,7 @@ def step_2(context: Context, scenario: Scenario, config: PolicyConfig) -> Scenar pass # Solution did not exist # Add this trajectory as bound_emission values - add_emission_trajectory( + add_emission_trajectory.main( scenario, data=df, type_emission="TCE_CO2", @@ -255,9 +261,8 @@ def step_2(context: Context, scenario: Scenario, config: PolicyConfig) -> Scenar with scenario.transact(message="Remove lower bound on global total CO₂ emissions"): name = "relation_lower" - scenario.remove_par( - name, scenario.par(name, filters={"relation": [RELATION_GLOBAL_CO2]}) - ) + filters = dict(relation=[context.model.relation_global_co2]) + scenario.remove_par(name, scenario.par(name, filters=filters)) return scenario @@ -305,9 +310,8 @@ def step_3(context: Context, scenario: Scenario, config: PolicyConfig) -> Scenar # As in step_2, remove the lower bound on global CO2 emissions. This is # necessary if step_2 was not run (for instance, NAVIGATE T6.2 protocol) name = "relation_lower" - scenario.remove_par( - name, scenario.par(name, filters={"relation": [RELATION_GLOBAL_CO2]}) - ) + filters = dict(relation=[context.model.relation_global_co2]) + scenario.remove_par(name, scenario.par(name, filters=filters)) return scenario @@ -391,7 +395,7 @@ def add_steps( # Add a step to solve the scenario (except for after step_0); update the # step name for the next loop iteration s = workflow.add_step( - f"{s} solved", s, solve, config=cfg, set_as_default=True + f"{s} solved", s, model_workflow.solve, config=cfg, set_as_default=True ) # Return the name of the last of the added steps From f9dbba828e8de0d5f28d84f5be6a1a36aaa8303b Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 09:25:25 +0200 Subject: [PATCH 07/10] Adjust .transport.workflow.tax_emission() for #350 --- message_ix_models/model/transport/workflow.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/message_ix_models/model/transport/workflow.py b/message_ix_models/model/transport/workflow.py index 4135ab772b..8122dccb54 100644 --- a/message_ix_models/model/transport/workflow.py +++ b/message_ix_models/model/transport/workflow.py @@ -143,20 +143,17 @@ def short_hash(value: str) -> str: def tax_emission(context: "Context", scenario: "Scenario", price: float) -> "Scenario": """Add emission tax. - This function calls code from :mod:`message_data.projects.navigate.workflow`, - :mod:`message_data.tools.utilities`, and other non-public locations. It cannot be - used without access to those codes. + See also + -------- + message_ix_models.project.engage.workflow.step_0 + message_ix_models.project.navigate.workflow.tax_emission """ from message_ix import make_df + from message_ix_models.project.engage import workflow as engage_workflow + from message_ix_models.project.navigate import workflow as navigate_workflow from message_ix_models.util import broadcast - try: - from message_data.projects.engage import workflow as engage_workflow - from message_data.projects.navigate import workflow as navigate_workflow - except ImportError: - raise RuntimeError("Requires non-public code from message_data") - # Add ENGAGE-style emissions accounting scenario = engage_workflow.step_0(context, scenario) @@ -266,7 +263,7 @@ def generate( lambda _, s: initial_new_capacity_up_v311(s, safety_factor=1.05), ) - # This block copied from message_data.projects.navigate.workflow + # This block copied from message_ix_models.project.navigate.workflow if config.policy: # Add a carbon tax name = wf.add_step(f"{label} with tax", name, tax_emission, price=1000.0) From d38f7e54dc38cbe5f72fa995e2dfb6bb82e57fee Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 22:10:10 +0200 Subject: [PATCH 08/10] Replace make_enum with an EnumType subclass - Use metaclass to construct an Enum subtype for e.g. SSP_2024. - Update tests. --- message_ix_models/project/ssp/structure.py | 20 +++- message_ix_models/tests/project/test_ssp.py | 3 +- message_ix_models/tests/util/test_sdmx.py | 124 +++++++++++--------- message_ix_models/util/sdmx.py | 96 +++++++++------ 4 files changed, 147 insertions(+), 96 deletions(-) diff --git a/message_ix_models/project/ssp/structure.py b/message_ix_models/project/ssp/structure.py index ddac63d0b3..d3e08302b2 100644 --- a/message_ix_models/project/ssp/structure.py +++ b/message_ix_models/project/ssp/structure.py @@ -8,7 +8,7 @@ import sdmx.urn from sdmx.model import common, v21 -from message_ix_models.util.sdmx import make_enum, register_agency, write +from message_ix_models.util.sdmx import ItemSchemeEnumType, read, register_agency, write if TYPE_CHECKING: from os import PathLike @@ -183,8 +183,18 @@ def generate(context: "Context", base_dir: Optional["PathLike"] = None): write(cl, base_dir) -#: Enumeration of codes for SSP 2017 edition. -SSP = SSP_2017 = make_enum("ICONICS:SSP(2017)") +class SSP_2017(metaclass=ItemSchemeEnumType): + """Enumeration of codes for SSP 2017 edition.""" -#: Enumeration of codes for SSP 2024 edition. -SSP_2024 = make_enum("ICONICS:SSP(2024)") + def _get_item_scheme(self): + return read("ICONICS:SSP(2017)") + + +SSP = SSP_2017 + + +class SSP_2024(metaclass=ItemSchemeEnumType): + """Enumeration of codes for SSP 2024 edition.""" + + def _get_item_scheme(self): + return read("ICONICS:SSP(2024)") diff --git a/message_ix_models/tests/project/test_ssp.py b/message_ix_models/tests/project/test_ssp.py index 3651fb144e..76911ae497 100644 --- a/message_ix_models/tests/project/test_ssp.py +++ b/message_ix_models/tests/project/test_ssp.py @@ -42,7 +42,8 @@ def test_enum(): # Same SSP ID from different enums are not equivalent assert SSP_2017["1"] != SSP_2024["1"] assert SSP_2017["1"] is not SSP_2024["1"] - assert SSP["1"] != SSP_2024["1"] + # NB Ignored because of https://github.com/python/mypy/issues/7568 + assert SSP["1"] != SSP_2024["1"] # type: ignore [misc] @pytest.mark.parametrize( diff --git a/message_ix_models/tests/util/test_sdmx.py b/message_ix_models/tests/util/test_sdmx.py index 9a90c3a537..2395d61136 100644 --- a/message_ix_models/tests/util/test_sdmx.py +++ b/message_ix_models/tests/util/test_sdmx.py @@ -14,7 +14,13 @@ data, # noqa: F401 testing, ) -from message_ix_models.util.sdmx import DATAFLOW, Dataflow, eval_anno, make_enum, read +from message_ix_models.util.sdmx import ( + DATAFLOW, + Dataflow, + ItemSchemeEnumType, + eval_anno, + read, +) log = logging.getLogger(__name__) @@ -116,6 +122,62 @@ def test_units(self, any_df: "Dataflow") -> None: assert isinstance(any_df.units, pint.Unit) +_urn_prefix = "urn:sdmx:org.sdmx.infomodel" + + +class TestItemSchemeEnum: + @pytest.mark.parametrize( + "urn, expected", + ( + ("ICONICS:SSP(2017)", f"{_urn_prefix}.codelist.Code=ICONICS:SSP(2017).1"), + ("ICONICS:SSP(2024)", f"{_urn_prefix}.codelist.Code=ICONICS:SSP(2024).1"), + ("SSP(2017)", f"{_urn_prefix}.codelist.Code=ICONICS:SSP(2017).1"), + ("SSP(2024)", f"{_urn_prefix}.codelist.Code=ICONICS:SSP(2024).1"), + ("SSP", f"{_urn_prefix}.codelist.Code=ICONICS:SSP(2017).1"), + ("AGENCIES", f"{_urn_prefix}.base.Agency=IIASA_ECE:AGENCIES(0.1).IEA"), + ), + ) + def test_new_class(self, urn: str, expected: str) -> None: + class Foo(metaclass=ItemSchemeEnumType): + def _get_item_scheme(self): + return read(urn) + + # A known URN retrieves an enumeration member + f = Foo.by_urn(expected) + assert isinstance(f, Foo) + + def test_bases(self) -> None: + """:func:`.make_enum` works with :class:`~enum.Flag` and subclasses.""" + from enum import Flag, IntFlag + + class E1(Flag, metaclass=ItemSchemeEnumType): + def _get_item_scheme(self): + return read("ICONICS:SSP(2017)") + + # Values are bitwise flags + assert not isinstance(E1["1"], int) + + # Expected length + assert 2 ** (len(E1) - 1) == list(E1)[-1].value # type: ignore [attr-defined] + + # Flags can be combined + flags = E1["1"] | E1["2"] + assert E1["1"] & flags + assert E1["2"] & flags + assert not (E1["3"] & flags) + + # Similar, with IntFlag + class E2(IntFlag, metaclass=ItemSchemeEnumType): + def _get_item_scheme(self): + return read("IIASA_ECE:AGENCIES(0.1)") + + # Values are ints + assert isinstance(E2["IIASA_ECE"], int) + + # Expected length + assert 2 ** (len(E2) - 1) == list(E2)[-1].value # type: ignore [attr-defined] + + def test_eval_anno(caplog, recwarn): c = Code() @@ -140,60 +202,6 @@ def test_eval_anno(caplog, recwarn): assert 7 == eval_anno(c, id="qux") -def test_make_enum0(): - """:func:`.make_enum` works with :class:`~enum.Flag` and subclasses.""" - from enum import Flag, IntFlag - - E = make_enum("ICONICS:SSP(2017)", base=Flag) - - # Values are bitwise flags - assert not isinstance(E["1"], int) - - # Expected length - assert 2 ** (len(E) - 1) == list(E)[-1].value - - # Flags can be combined - flags = E["1"] | E["2"] - assert E["1"] & flags - assert E["2"] & flags - assert not (E["3"] & flags) - - # Similar, with IntFlag - E = make_enum("IIASA_ECE:AGENCIES(0.1)", base=IntFlag) - - # Values are ints - assert isinstance(E["IIASA_ECE"], int) - - # Expected length - assert 2 ** (len(E) - 1) == list(E)[-1].value - - -_urn_prefix = "urn:sdmx:org.sdmx.infomodel.codelist" - - -@pytest.mark.parametrize( - "urn, expected", - ( - ("ICONICS:SSP(2017)", f"{_urn_prefix}.Code=ICONICS:SSP(2017).1"), - ("ICONICS:SSP(2024)", f"{_urn_prefix}.Code=ICONICS:SSP(2024).1"), - ("SSP(2017)", f"{_urn_prefix}.Code=ICONICS:SSP(2017).1"), - ("SSP(2024)", f"{_urn_prefix}.Code=ICONICS:SSP(2024).1"), - ("SSP", f"{_urn_prefix}.Code=ICONICS:SSP(2017).1"), - pytest.param( - "AGENCIES", - f"{_urn_prefix}.Agency=IIASA_ECE:AGENCIES(0.1).IEA", - marks=pytest.mark.xfail(raises=KeyError, reason="XML needs update"), - ), - ), -) -def test_make_enum1(urn, expected): - # make_enum() runs - E = make_enum(urn) - - # A known URN retrieves an enumeration member - E.by_urn(expected) - - @pytest.mark.parametrize( "urn, expected", ( @@ -203,14 +211,16 @@ def test_make_enum1(urn, expected): ("SSP(2024)", "Codelist=ICONICS:SSP(2024)"), ("SSP", "Codelist=ICONICS:SSP(2017)"), ("AGENCIES", "AgencyScheme=IIASA_ECE:AGENCIES(0.1)"), + ("IIASA_ECE:AGENCIES", "AgencyScheme=IIASA_ECE:AGENCIES(0.1)"), + ("IIASA_ECE:AGENCIES(0.1)", "AgencyScheme=IIASA_ECE:AGENCIES(0.1)"), ), ) -def test_read0(urn, expected): +def test_read0(urn: str, expected: str) -> None: obj = read(urn) assert expected in obj.urn -def test_read1(): +def test_read1() -> None: SSPS = read("ssp") # Identify an SSP by matching strings in its name diff --git a/message_ix_models/util/sdmx.py b/message_ix_models/util/sdmx.py index 5cf8291314..3c5603b145 100644 --- a/message_ix_models/util/sdmx.py +++ b/message_ix_models/util/sdmx.py @@ -5,11 +5,11 @@ from collections.abc import Iterable, Mapping from dataclasses import dataclass, fields from datetime import datetime -from enum import Enum, Flag +from enum import Enum, EnumType, Flag, auto from functools import cache from importlib.metadata import version from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union, cast +from typing import TYPE_CHECKING, Optional, TypeVar, Union, cast from warnings import warn import sdmx @@ -25,7 +25,6 @@ if TYPE_CHECKING: from os import PathLike - from typing import TypeVar import pint from genno import Computer, Key @@ -36,11 +35,11 @@ # TODO Use "from typing import Self" once Python 3.11 is the minimum supported Self = TypeVar("Self", bound="AnnotationsMixIn") + log = logging.getLogger(__name__) CodeLike = Union[str, common.Code] - #: Collection of :class:`.Dataflow` instances. DATAFLOW: dict[str, "Dataflow"] = {} @@ -399,19 +398,70 @@ def generate_csv_template(self) -> Path: # template = -class URNLookupEnum(Enum): - """:class:`.Enum` subclass that allows looking up members using a URN.""" - - _ignore_ = "_urn_name" - _urn_name: dict +T = TypeVar("T", bound=Enum) - def __init_subclass__(cls): - cls._urn_name = dict() +class URNLookupMixin[T]: @classmethod - def by_urn(cls, urn: str): + def by_urn(cls, urn: str) -> T: """Return the :class:`.Enum` member given its `urn`.""" - return cls[cls.__dict__["_urn_name"][urn]] + name = cls.__dict__["_urn_name"][urn] + return cls.__dict__["_member_map_"][name] + + +class URNLookupEnum(URNLookupMixin, Enum): + """Class constructed by ItemSchemeEnumType.""" + + +class ItemSchemeEnumType(EnumType): + @classmethod + def __prepare__(metacls, cls, bases, **kwgs): + return {} + + def __init__(cls, *args, **kwds): + super(ItemSchemeEnumType, cls).__init__(*args) + + def __new__(metacls, cls, bases, dct, **kwargs) -> type["URNLookupEnum"]: + # Retrieve the item scheme + scheme = dct.pop("_get_item_scheme")(None) + if not isinstance(scheme, common.ItemScheme): + raise RuntimeError( + f"Callback for {cls} returned {scheme}; expected ItemScheme" + ) + + # Prepend URNLookupMixin to the base class(es); use Enum as a default + bases = (URNLookupMixin,) + (bases or (Enum,)) + + # Prepare the EnumDict for creating the class + enum_dct = super(ItemSchemeEnumType, metacls).__prepare__(cls, bases, **kwargs) + # Transfer class dct private members + enum_dct.update(dct) + + # Populate the class member dictionary and URN → member name mapping + _urn_name = dict() + + if any(issubclass(c, Flag) for c in bases): + # Ensure the 0 member is NONE, not any of the codes + enum_dct["NONE"] = 0 + for i, item in enumerate(scheme, start=1): + _urn_name[item.urn] = item.id + enum_dct[item.id] = auto() + + # Create the class + enum_class = cast( + type["URNLookupEnum"], + super(ItemSchemeEnumType, metacls).__new__( + metacls, cls, bases, enum_dct, **kwargs + ), + ) + + # Store the _urn_name mapping + setattr(enum_class, "_urn_name", _urn_name) + + return enum_class + + # NB Provided solely to satisfy mypy, never called + def by_urn(self, urn: str) -> "URNLookupEnum": ... # type: ignore [empty-body] # FIXME Reduce complexity from 13 → ≤11 @@ -712,26 +762,6 @@ def get_version(with_dev: Optional[bool] = True) -> str: return str(common.Version(tmp)) -def make_enum(urn, base=URNLookupEnum): - """Create an :class:`.enum.Enum` (or `base`) with members from codelist `urn`.""" - # Read the code list - cl = read(urn) - - # Ensure the 0 member is NONE, not any of the codes - names = ["NONE"] if issubclass(base, Flag) else [] - names.extend(code.id for code in cl) - - # Create the class - result = base(urn, names) - - if issubclass(base, URNLookupEnum): - # Populate the URN → member name mapping - for code in cl: - result._urn_name[code.urn] = code.id - - return result - - def read(urn: str, base_dir: Optional["PathLike"] = None): """Read SDMX object from package data given its `urn`.""" # Identify a path that matches `urn` From 19731a528c466e6cd8d2f6c7b00632a6ab522986 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 09:38:34 +0200 Subject: [PATCH 09/10] Satisfy mypy in .project.ssp.__init__ This change avoids sporadic local mypy failures via pre-commit. --- message_ix_models/project/ssp/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/message_ix_models/project/ssp/__init__.py b/message_ix_models/project/ssp/__init__.py index 477a01117b..ed13d53275 100644 --- a/message_ix_models/project/ssp/__init__.py +++ b/message_ix_models/project/ssp/__init__.py @@ -1,9 +1,12 @@ import logging import re -from typing import Union +from typing import TYPE_CHECKING, Union from .structure import SSP, SSP_2017, SSP_2024, generate +if TYPE_CHECKING: + from message_ix_models.util.sdmx import URNLookupEnum + __all__ = [ "SSP", "SSP_2017", @@ -38,7 +41,7 @@ def __init__(self, default: Union[SSP_2017, SSP_2024]): def __set_name__(self, owner, name): self._name = "_" + name - def __get__(self, obj, type) -> Union[SSP_2017, SSP_2024]: + def __get__(self, obj, type) -> "URNLookupEnum": if obj is None: return None # type: ignore [return-value] From b293ee21a7a4402403c1effc4cb6d55ce8d04407 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 19 May 2025 22:22:55 +0200 Subject: [PATCH 10/10] Add item URNs to IIASA_ECE:AGENCIES --- .../data/sdmx/IIASA_ECE_AGENCIES(0.1).xml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/message_ix_models/data/sdmx/IIASA_ECE_AGENCIES(0.1).xml b/message_ix_models/data/sdmx/IIASA_ECE_AGENCIES(0.1).xml index 64b57345b9..0ac08c7bc9 100644 --- a/message_ix_models/data/sdmx/IIASA_ECE_AGENCIES(0.1).xml +++ b/message_ix_models/data/sdmx/IIASA_ECE_AGENCIES(0.1).xml @@ -1,14 +1,17 @@ - + + + none false - 2023-09-04T16:31:44.700655 - Generated by message_ix_models 2023.5.32.dev20+g8d51636 + 2025-05-19T22:09:29.424849 + + Generated by message_ix_models 2025.1.11.dev398+gdaf97af4a.d20250504 - + Agencies referenced by data structures in message_ix_models - + IIASA Energy, Climate, and Environment Program @@ -17,7 +20,7 @@ https://depts.washington.edu/iconics/ - + International Energy Agency https://iea.org