Repository: IAMconsortium/nomenclature

Issue:
Using only country in nomenclature config breaks CodeList
I saw this in https://github.com/iiasa/openmod4africa-workflow, where the latest nomenclature update in combination with a nomenclature config where in the region dimension there's only the country attribute:

```yaml
repositories:
  openENTRANCE:
    url: https://github.com/openENTRANCE/openentrance
definitions:
  variable:
    repository: openENTRANCE
  region:
    country: true
```

It broke in the test here: https://github.com/iiasa/openmod4africa-workflow/actions/runs/7829213013/job/21360733654. 

Issue is that https://github.com/IAMconsortium/nomenclature/blob/86d38f9b4457f897445bee387554d35e45ae873e/nomenclature/codelist.py#L622 
attempts to iterate over the region.repositories attribute which is empty.

Discussion:

Patch:
diff --git a/nomenclature/codelist.py b/nomenclature/codelist.py
index 9c8e158..b26fea8 100644
--- a/nomenclature/codelist.py
+++ b/nomenclature/codelist.py
@@ -1,4 +1,3 @@
-from contextlib import suppress
 import logging
 from pathlib import Path
 from typing import ClassVar, Dict, List
@@ -6,15 +5,14 @@ from typing import ClassVar, Dict, List
 import numpy as np
 import pandas as pd
 import yaml
-from pyam.utils import write_sheet
-from pydantic import field_validator, BaseModel, ValidationInfo
+from pyam.utils import is_list_like, write_sheet
+from pydantic import BaseModel, ValidationInfo, field_validator
 from pydantic_core import PydanticCustomError
 
 import nomenclature
 from nomenclature.code import Code, MetaCode, RegionCode, VariableCode
-from nomenclature.config import NomenclatureConfig
-from nomenclature.error import custom_pydantic_errors, ErrorCollector
-from pyam.utils import is_list_like
+from nomenclature.config import CodeListConfig, NomenclatureConfig
+from nomenclature.error import ErrorCollector, custom_pydantic_errors
 
 here = Path(__file__).parent.absolute()
 
@@ -198,21 +196,14 @@ class CodeList(BaseModel):
 
         """
         code_list = cls._parse_codelist_dir(path, file_glob_pattern)
-
-        with suppress(AttributeError):
-            dimension = path.name
-            codelistconfig = getattr(config.definitions, dimension)
-            for repo in codelistconfig.repositories:
-                repo_path = (
-                    config.repositories[repo].local_path / "definitions" / dimension
-                )
-                code_list = (
-                    cls._parse_codelist_dir(
-                        repo_path,
-                        file_glob_pattern,
-                    )
-                    + code_list
-                )
+        config = config or NomenclatureConfig()
+        for repo in getattr(
+            config.definitions, name.lower(), CodeListConfig()
+        ).repositories:
+            repo_path = config.repositories[repo].local_path / "definitions" / name
+            code_list = (
+                cls._parse_codelist_dir(repo_path, file_glob_pattern) + code_list
+            )
         errors = ErrorCollector()
         mapping: Dict[str, Code] = {}
         for code in code_list:
@@ -604,35 +595,33 @@ class RegionCodeList(CodeList):
         code_list: List[RegionCode] = []
 
         # initializing from general configuration
-        with suppress(AttributeError):
-            # adding all countries
-            if config.definitions.region.country is True:
-                for c in nomenclature.countries:
-                    try:
-                        code_list.append(
-                            RegionCode(
-                                name=c.name, iso3_codes=c.alpha_3, hierarchy="Country"
-                            )
+        # adding all countries
+        config = config or NomenclatureConfig()
+        if config.definitions.region.country is True:
+            for c in nomenclature.countries:
+                try:
+                    code_list.append(
+                        RegionCode(
+                            name=c.name, iso3_codes=c.alpha_3, hierarchy="Country"
                         )
-                    # special handling for countries that do not have an alpha_3 code
-                    except AttributeError:
-                        code_list.append(RegionCode(name=c.name, hierarchy="Country"))
-
-            # importing from an external repository
-            for repo in config.definitions.region.repositories:
-                repo_path = (
-                    config.repositories[repo].local_path / "definitions" / "region"
-                )
-
-                code_list = cls._parse_region_code_dir(
-                    code_list,
-                    repo_path,
-                    file_glob_pattern,
-                    repository=config.definitions.region.repositories,
-                )
-                code_list = cls._parse_and_replace_tags(
-                    code_list, repo_path, file_glob_pattern
-                )
+                    )
+                # special handling for countries that do not have an alpha_3 code
+                except AttributeError:
+                    code_list.append(RegionCode(name=c.name, hierarchy="Country"))
+
+        # importing from an external repository
+        for repo in config.definitions.region.repositories:
+            repo_path = config.repositories[repo].local_path / "definitions" / "region"
+
+            code_list = cls._parse_region_code_dir(
+                code_list,
+                repo_path,
+                file_glob_pattern,
+                repository=config.definitions.region.repositories,
+            )
+            code_list = cls._parse_and_replace_tags(
+                code_list, repo_path, file_glob_pattern
+            )
 
         # parse from current repository
         code_list = cls._parse_region_code_dir(code_list, path, file_glob_pattern)
diff --git a/nomenclature/config.py b/nomenclature/config.py
index 3971c1d..0bfb112 100644
--- a/nomenclature/config.py
+++ b/nomenclature/config.py
@@ -27,9 +27,9 @@ def convert_to_set(v: str | list[str] | set[str]) -> set[str]:
 
 
 class CodeListConfig(BaseModel):
-    dimension: str
-    repositories: Annotated[set[str] | None, BeforeValidator(convert_to_set)] = Field(
-        None, alias="repository"
+    dimension: str | None = None
+    repositories: Annotated[set[str], BeforeValidator(convert_to_set)] = Field(
+        default_factory=set, alias="repository"
     )
     model_config = ConfigDict(populate_by_name=True)
 
@@ -94,8 +94,8 @@ class DataStructureConfig(BaseModel):
 
     """
 
-    region: Optional[RegionCodeListConfig] = None
-    variable: Optional[CodeListConfig] = None
+    region: Optional[RegionCodeListConfig] = Field(default_factory=RegionCodeListConfig)
+    variable: Optional[CodeListConfig] = Field(default_factory=CodeListConfig)
 
     @field_validator("region", "variable", mode="before")
     @classmethod
@@ -107,30 +107,29 @@ class DataStructureConfig(BaseModel):
         return {
             dimension: getattr(self, dimension).repositories
             for dimension in ("region", "variable")
-            if getattr(self, dimension) and getattr(self, dimension).repositories
+            if getattr(self, dimension).repositories
         }
 
 
 class RegionMappingConfig(BaseModel):
     repositories: Annotated[set[str], BeforeValidator(convert_to_set)] = Field(
-        ..., alias="repository"
+        default_factory=set, alias="repository"
     )
     model_config = ConfigDict(populate_by_name=True)
 
 
 class NomenclatureConfig(BaseModel):
-    repositories: dict[str, Repository] = {}
-    definitions: Optional[DataStructureConfig] = None
-    mappings: Optional[RegionMappingConfig] = None
+    repositories: dict[str, Repository] = Field(default_factory=dict)
+    definitions: DataStructureConfig = Field(default_factory=DataStructureConfig)
+    mappings: RegionMappingConfig = Field(default_factory=RegionMappingConfig)
 
     @model_validator(mode="after")
     @classmethod
     def check_definitions_repository(
         cls, v: "NomenclatureConfig"
     ) -> "NomenclatureConfig":
-        definitions_repos = v.definitions.repos if v.definitions else {}
         mapping_repos = {"mappings": v.mappings.repositories} if v.mappings else {}
-        repos = {**definitions_repos, **mapping_repos}
+        repos = {**v.definitions.repos, **mapping_repos}
         for use, repositories in repos.items():
             if repositories - v.repositories.keys():
                 raise ValueError((f"Unknown repository {repositories} in '{use}'."))
diff --git a/nomenclature/definition.py b/nomenclature/definition.py
index e1eda28..e6134a1 100644
--- a/nomenclature/definition.py
+++ b/nomenclature/definition.py
@@ -44,9 +44,11 @@ class DataStructureDefinition:
         if (file := path.parent / "nomenclature.yaml").exists():
             self.config = NomenclatureConfig.from_file(file=file)
         else:
-            self.config = None
+            self.config = NomenclatureConfig()
 
-        if not path.is_dir() and (self.config is None or not self.config.repositories):
+        if not path.is_dir() and not (
+            self.config.repositories or self.config.definitions.region.country
+        ):
             raise NotADirectoryError(f"Definitions directory not found: {path}")
 
         self.dimensions = dimensions or ["region", "variable"]
diff --git a/nomenclature/processor/region.py b/nomenclature/processor/region.py
index 99764cd..eee5ec6 100644
--- a/nomenclature/processor/region.py
+++ b/nomenclature/processor/region.py
@@ -483,15 +483,14 @@ class RegionProcessor(Processor):
 
         mapping_files = [f for f in path.glob("**/*") if f.suffix in {".yaml", ".yml"}]
 
-        if dsd.config and dsd.config.mappings:
-            for repository in dsd.config.mappings.repositories:
-                mapping_files.extend(
-                    f
-                    for f in (
-                        dsd.config.repositories[repository].local_path / "mappings"
-                    ).glob("**/*")
-                    if f.suffix in {".yaml", ".yml"}
-                )
+        for repository in dsd.config.mappings.repositories:
+            mapping_files.extend(
+                f
+                for f in (
+                    dsd.config.repositories[repository].local_path / "mappings"
+                ).glob("**/*")
+                if f.suffix in {".yaml", ".yml"}
+            )
 
         for file in mapping_files:
             try:


Test patch:
diff --git a/tests/data/general-config-only-country/nomenclature.yaml b/tests/data/general-config-only-country/nomenclature.yaml
new file mode 100644
index 0000000..5217775
--- /dev/null
+++ b/tests/data/general-config-only-country/nomenclature.yaml
@@ -0,0 +1,3 @@
+definitions:
+  region:
+    country: true
diff --git a/tests/test_definition.py b/tests/test_definition.py
index 2de7187..87dacba 100644
--- a/tests/test_definition.py
+++ b/tests/test_definition.py
@@ -64,6 +64,14 @@ def test_definition_from_general_config(workflow_folder):
         clean_up_external_repos(obs.config.repositories)
 
 
+def test_definition_general_config_country_only():
+    obs = DataStructureDefinition(
+        TEST_DATA_DIR / "general-config-only-country" / "definitions",
+        dimensions=["region"],
+    )
+    assert all(region in obs.region for region in ("Austria", "Bolivia", "Kosovo"))
+
+
 def test_to_excel(simple_definition, tmpdir):
     """Check writing a DataStructureDefinition to file"""
     file = tmpdir / "testing_export.xlsx"