Repository: IAMconsortium/nomenclature

Issue:
Raise all duplicate-errors during initialization
Currently, only a unique duplicate-code error is raised during initialization (e.g., from `validate-project`). It would be great to collect all duplicates and raise one pydantic-error-collection.

Discussion:

Patch:
diff --git a/nomenclature/codelist.py b/nomenclature/codelist.py
index 7cb3994..9c8e158 100644
--- a/nomenclature/codelist.py
+++ b/nomenclature/codelist.py
@@ -13,7 +13,7 @@ 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
+from nomenclature.error import custom_pydantic_errors, ErrorCollector
 from pyam.utils import is_list_like
 
 here = Path(__file__).parent.absolute()
@@ -213,12 +213,16 @@ class CodeList(BaseModel):
                     )
                     + code_list
                 )
-
+        errors = ErrorCollector()
         mapping: Dict[str, Code] = {}
         for code in code_list:
             if code.name in mapping:
-                raise ValueError(f"Duplicate item in {name} codelist: {code.name}")
+                errors.append(
+                    ValueError(f"Duplicate item in {name} codelist: {code.name}")
+                )
             mapping[code.name] = code
+        if errors:
+            raise ValueError(errors)
         return cls(name=name, mapping=mapping)
 
     @classmethod
@@ -636,11 +640,14 @@ class RegionCodeList(CodeList):
 
         # translate to mapping
         mapping: Dict[str, RegionCode] = {}
+
+        errors = ErrorCollector()
         for code in code_list:
             if code.name in mapping:
-                raise ValueError(f"Trying to set a duplicate code {code.name}")
+                errors.append(ValueError(f"Trying to set a duplicate code {code.name}"))
             mapping[code.name] = code
-
+        if errors:
+            raise ValueError(errors)
         return cls(name=name, mapping=mapping)
 
     @property
diff --git a/nomenclature/error.py b/nomenclature/error.py
index 2bf2c6c..39296ce 100644
--- a/nomenclature/error.py
+++ b/nomenclature/error.py
@@ -1,3 +1,4 @@
+import textwrap
 from collections import namedtuple
 
 pydantic_custom_error_config = {
@@ -55,8 +56,12 @@ class ErrorCollector:
 
     def __repr__(self) -> str:
         error = "error" if len(self.errors) == 1 else "errors"
-        return f"Collected {len(self.errors)} {error}:\n" + "\n\t".join(
-            str(error) for error in self.errors
+        error_list_str = "\n".join(
+            f"{i+1}. {error}" for i, error in enumerate(self.errors)
+        )
+
+        return f"Collected {len(self.errors)} {error}:\n" + textwrap.indent(
+            error_list_str, prefix="  "
         )
 
     def __bool__(self) -> bool:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ecc90ce..156a440 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,4 @@
 import shutil
-import sys
 import os
 import stat
 from pathlib import Path
diff --git a/tests/data/duplicate-code-list/variable/duplicate_entries.yaml b/tests/data/duplicate-code-list/variable/duplicate_entries.yaml
new file mode 100644
index 0000000..0c945f2
--- /dev/null
+++ b/tests/data/duplicate-code-list/variable/duplicate_entries.yaml
@@ -0,0 +1,12 @@
+- Some Variable:
+    description:
+    unit:
+- Some Variable:
+    description:
+    unit:
+- Some other Variable:
+    description:
+    unit:
+- Some other Variable:
+    description:
+    unit:
diff --git a/tests/test_codelist.py b/tests/test_codelist.py
index 5668bc6..5ebcad8 100644
--- a/tests/test_codelist.py
+++ b/tests/test_codelist.py
@@ -1,6 +1,7 @@
 from pytest import raises
 import pandas as pd
 import pandas.testing as pdt
+import pytest
 import logging
 
 from nomenclature.code import Code, RegionCode, MetaCode
@@ -331,11 +332,29 @@ def test_multiple_external_repos():
         TEST_DATA_DIR / "nomenclature_configs" / "multiple_repos_per_dimension.yaml"
     )
     try:
-        with raises(ValueError, match="Duplicate"):
-            variable_code_list = VariableCodeList.from_directory(
-                "variable",
-                TEST_DATA_DIR / "nomenclature_configs" / "variable",
-                nomenclature_config,
-            )
+        variable_code_list = VariableCodeList.from_directory(
+            "variable",
+            TEST_DATA_DIR / "nomenclature_configs" / "variable",
+            nomenclature_config,
+        )
+        assert nomenclature_config.repositories.keys() == {
+            "common-definitions",
+            "legacy-definitions",
+        }
+
+        assert all(
+            repo.local_path.is_dir()
+            for repo in nomenclature_config.repositories.values()
+        )
+        assert len(variable_code_list) > 2000
     finally:
         clean_up_external_repos(nomenclature_config.repositories)
+
+
+@pytest.mark.parametrize("CodeList", [VariableCodeList, CodeList])
+def test_variable_codelist_with_duplicates_raises(CodeList):
+    error_string = "2 errors:\n.*Some Variable\n.*Some other Variable"
+    with raises(ValueError, match=error_string):
+        CodeList.from_directory(
+            "variable", TEST_DATA_DIR / "duplicate-code-list" / "variable"
+        )