Repository: IAMconsortium/nomenclature

Issue:
Better formatting for error messages
Some error messages are currently somewhat hard to read, example:

```console
pydantic.error_wrappers.ValidationError: 1 validation error for RegionProcessor
mappings -> ['REMIND 3.0']
  Region(s) ['REMIND 3.0|Canada, NZ, Australia', 'REMIND 3.0|China', 'REMIND 3.0|EU 28', 'REMIND 3.0|India', 'REMIND 3.0|Japan', 'REMIND 3.0|Latin America and the Caribbean', 'REMIND 3.0|Middle East, North Africa, Central Asia', 'REMIND 3.0|Non-EU28 Europe', 'REMIND 3.0|other Asia', 'REMIND 3.0|Countries from the Reforming Economies of the Former Soviet Union', 'REMIND 3.0|Sub-saharan Africa', 'REMIND 3.0|United States of America'] in mappings/REMIND+3.0.yml not defined in the DataStructureDefinition (type=value_error.region_not_defined; region=['REMIND 3.0|Canada, NZ, Australia', 'REMIND 3.0|China', 'REMIND 3.0|EU 28', 'REMIND 3.0|India', 'REMIND 3.0|Japan', 'REMIND 3.0|Latin America and the Caribbean', 'REMIND 3.0|Middle East, North Africa, Central Asia', 'REMIND 3.0|Non-EU28 Europe', 'REMIND 3.0|other Asia', 'REMIND 3.0|Countries from the Reforming Economies of the Former Soviet Union', 'REMIND 3.0|Sub-saharan Africa', 'REMIND 3.0|United States of America']; file=mappings/REMIND+3.0.yml)
```

simply introducing a few strategically placed line breaks should make things better:

```console
pydantic.error_wrappers.ValidationError: 1 validation error for RegionProcessor
mappings -> ['REMIND 3.0']
Region(s):
 ['REMIND 3.0|Canada, NZ, Australia', 'REMIND 3.0|China', 'REMIND 3.0|EU 28', 'REMIND 3.0|India', 'REMIND 3.0|Japan', 'REMIND 3.0|Latin America and the Caribbean', 'REMIND 3.0|Middle East, North Africa, Central Asia', 'REMIND 3.0|Non-EU28 Europe', 'REMIND 3.0|other Asia', 'REMIND 3.0|Countries from the Reforming Economies of the Former Soviet Union', 'REMIND 3.0|Sub-saharan Africa', 'REMIND 3.0|United States of America'] 
    in mappings/REMIND+3.0.yml 
not defined in the DataStructureDefinition 
(type=value_error.region_not_defined; region=['REMIND 3.0|Canada, NZ, Australia', 'REMIND 3.0|China', 'REMIND 3.0|EU 28', 'REMIND 3.0|India', 'REMIND 3.0|Japan', 'REMIND 3.0|Latin America and the Caribbean', 'REMIND 3.0|Middle East, North Africa, Central Asia', 'REMIND 3.0|Non-EU28 Europe', 'REMIND 3.0|other Asia', 'REMIND 3.0|Countries from the Reforming Economies of the Former Soviet Union', 'REMIND 3.0|Sub-saharan Africa', 'REMIND 3.0|United States of America']; file=mappings/REMIND+3.0.yml)
```
or something along those lines.

Discussion:

Patch:
diff --git a/nomenclature/code.py b/nomenclature/code.py
index 097b644..0d7e536 100644
--- a/nomenclature/code.py
+++ b/nomenclature/code.py
@@ -12,6 +12,8 @@ from pydantic import (
     ValidationInfo,
 )
 
+from nomenclature.error import ErrorCollector
+
 from pyam.utils import to_list
 
 from .countries import countries
@@ -259,15 +261,20 @@ class RegionCode(Code):
     @field_validator("iso3_codes")
     def check_iso3_codes(cls, v: List[str], info: ValidationInfo) -> List[str]:
         """Verifies that each ISO3 code is valid according to pycountry library."""
+        errors = ErrorCollector()
         if invalid_iso3_codes := [
             iso3_code
             for iso3_code in to_list(v)
             if countries.get(alpha_3=iso3_code) is None
         ]:
-            raise ValueError(
-                f"Region '{info.data['name']}' has invalid ISO3 country code(s): "
-                + ", ".join(invalid_iso3_codes)
+            errors.append(
+                ValueError(
+                    f"Region '{info.data['name']}' has invalid ISO3 country code(s): "
+                    + ", ".join(invalid_iso3_codes)
+                )
             )
+        if errors:
+            raise ValueError(errors)
         return v
 
 
diff --git a/nomenclature/error.py b/nomenclature/error.py
index 3ae8750..6549a90 100644
--- a/nomenclature/error.py
+++ b/nomenclature/error.py
@@ -39,7 +39,7 @@ pydantic_custom_error_config = {
     ),
     "RegionNotDefinedError": (
         "region_not_defined",
-        "Region(s) {regions} in {file} not found in RegionCodeList",
+        "Region(s)\n{regions}\nin {file}\nnot found in RegionCodeList",
     ),
 }
 


Test patch:
diff --git a/tests/test_code.py b/tests/test_code.py
index c228c5e..25380b7 100644
--- a/tests/test_code.py
+++ b/tests/test_code.py
@@ -157,7 +157,8 @@ def test_RegionCode_iso3_code_list_fail():
     error_pattern = (
         "1 validation error for RegionCode\n"
         "iso3_codes\n"
-        "  Value error, Region 'Western Europe' has "
+        "  Value error, Collected 1 error:\n"
+        "  1. Region 'Western Europe' has "
         "invalid ISO3 country code\(s\): DMK, IPL, ATZ, FNL, FRE, DEX, GRE, "  # noqa
         "IBL, ITL, LIC, MLA, BEG, FRT, ANB, GDR, LXB, MNO, NTD, NRW, PRE, EPA, "  # noqa
         "SWD, CEW, GTR, SOR"  # noqa
@@ -170,7 +171,8 @@ def test_RegionCode_iso3_code_str_fail():
     error_pattern = (
         "1 validation error for RegionCode\n"
         "iso3_codes\n"
-        "  Value error, Region 'Austria' has invalid ISO3 country code\(s\): AUTT"
+        "  Value error, Collected 1 error:\n"
+        "  1. Region 'Austria' has invalid ISO3 country code\(s\): AUTT"
     )
     with raises(ValueError, match=error_pattern):
         RegionCode(name="Austria", hierarchy="country", iso3_codes="AUTT")
diff --git a/tests/test_region_aggregation.py b/tests/test_region_aggregation.py
index f10c7d8..bdeea94 100644
--- a/tests/test_region_aggregation.py
+++ b/tests/test_region_aggregation.py
@@ -144,9 +144,9 @@ def test_region_processor_not_defined(simple_definition):
     # definition
     error_msg = (
         "mappings.model_(a|b).*\n"
-        ".*region_a.*mapping_(1|2).yaml.*region_not_defined.*\n"
+        ".*\n.*region_a.*\n.*mapping_(1|2).yaml\n.*region_not_defined.*\n"
         "mappings.model_(a|b).*\n"
-        ".*region_a.*mapping_(1|2).yaml.*region_not_defined"
+        ".*\n.*region_a.*\n.*mapping_(1|2).yaml\n.*region_not_defined"
     )
 
     with pytest.raises(ValueError, match=error_msg):