diff --git a/nomenclature/error.py b/nomenclature/error.py index 4cdca95f..b240277e 100644 --- a/nomenclature/error.py +++ b/nomenclature/error.py @@ -40,6 +40,10 @@ "region_not_defined", "Region(s)\n{regions}\nin {file}\nnot found in RegionCodeList", ), + "ConstituentsNotNativeError": ( + "constituents_not_native", + "Constituent region(s)\n{regions}\nin {file} not found in RegionCodeList", + ), } PydanticCustomErrors = namedtuple("PydanticCustomErrors", pydantic_custom_error_config) diff --git a/nomenclature/processor/region.py b/nomenclature/processor/region.py index e853e648..19e238ae 100644 --- a/nomenclature/processor/region.py +++ b/nomenclature/processor/region.py @@ -232,6 +232,21 @@ def check_exclude_common_region_overlap( ) -> "RegionAggregationMapping": return _check_exclude_region_overlap(v, "common_regions") + @model_validator(mode="after") + @classmethod + def check_constituent_regions_in_native_regions( + cls, v: "RegionAggregationMapping" + ) -> "RegionAggregationMapping": + if v.common_regions and v.native_regions: + if missing := set( + [cr for r in v.common_regions for cr in r.constituent_regions] + ).difference([r.name for r in v.native_regions if v.native_regions]): + raise PydanticCustomError( + *custom_pydantic_errors.ConstituentsNotNativeError, + {"regions": missing, "file": v.file}, + ) + return v + @classmethod def from_file(cls, file: Path | str) -> "RegionAggregationMapping": """Initialize a RegionAggregationMapping from a file. diff --git a/tests/data/cli/structure_validation_fails/mappings/mapping_2.yaml b/tests/data/cli/structure_validation_fails/mappings/mapping_2.yaml index 1c171c6a..be91833e 100644 --- a/tests/data/cli/structure_validation_fails/mappings/mapping_2.yaml +++ b/tests/data/cli/structure_validation_fails/mappings/mapping_2.yaml @@ -4,4 +4,3 @@ native_regions: common_regions: - World: - region_a - - region_b diff --git a/tests/data/region_processing/region_aggregation/illegal_mapping_constituent_native_missing.yaml b/tests/data/region_processing/region_aggregation/illegal_mapping_constituent_native_missing.yaml new file mode 100644 index 00000000..838a5326 --- /dev/null +++ b/tests/data/region_processing/region_aggregation/illegal_mapping_constituent_native_missing.yaml @@ -0,0 +1,10 @@ +# Constituent region is not defined in native region +model: model_a +native_regions: + - region_a: alternative_name_a + - region_b: alternative_name_b +common_regions: + - common_region_1: + - region_a + - region_b + - region_c diff --git a/tests/test_region_aggregation.py b/tests/test_region_aggregation.py index 5640b71f..e0bc49e5 100644 --- a/tests/test_region_aggregation.py +++ b/tests/test_region_aggregation.py @@ -83,6 +83,10 @@ def test_mapping(): "illegal_mapping_model_only.yaml", "one of 'native_regions' and 'common_regions'", ), + ( + "illegal_mapping_constituent_native_missing.yaml", + "Constituent region\(s\)\n.*\n", + ), ], ) def test_illegal_mappings(file, error_msg_pattern): @@ -191,7 +195,7 @@ def test_region_processor_wrong_args(): def test_region_processor_multiple_wrong_mappings(simple_definition): # Read in the entire region_aggregation directory and return **all** errors - msg = "Collected 9 errors" + msg = "Collected 10 errors" with pytest.raises(ValueError, match=msg): RegionProcessor.from_directory(