From 56759526980549941c9fcf8fcf7f10c7acc3569d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 25 Mar 2022 00:33:42 +0100 Subject: [PATCH] Improve detailed validation for external validators --- src/cattrs/gen.py | 18 ++++++++++++++---- tests/test_validation.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index f4266ffb..3027cf30 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -215,7 +215,7 @@ def make_dict_structure_fn( _cattrs_use_linecache: bool = True, _cattrs_prefer_attrib_converters: bool = False, _cattrs_detailed_validation: bool = True, - **kwargs, + **kwargs: AttributeOverride, ) -> Callable[[Mapping[str, Any], Any], T]: """Generate a specialized dict structuring function for an attrs class.""" @@ -326,6 +326,15 @@ def make_dict_structure_fn( post_lines.append( f" if errors: raise __c_cve('While structuring {cl.__name__}', errors, __cl)" ) + instantiation_lines = ( + [" try:"] + + [" return __cl("] + + [f" {line}" for line in invocation_lines] + + [" )"] + + [ + f" except Exception as exc: raise __c_cve('While structuring {cl.__name__}', [exc], __cl)" + ] + ) else: non_required = [] # The first loop deals with required args. @@ -432,6 +441,9 @@ def make_dict_structure_fn( ) else: post_lines.append(f" res['{ian}'] = o['{kn}']") + instantiation_lines = ( + [" return __cl("] + [f" {line}" for line in invocation_lines] + [" )"] + ) if _cattrs_forbid_extra_keys: globs["__c_a"] = allowed_fields @@ -452,9 +464,7 @@ def make_dict_structure_fn( [f"def {fn_name}(o, _, *, {internal_arg_line}):"] + lines + post_lines - + [" return __cl("] - + [f" {line}" for line in invocation_lines] - + [" )"] + + instantiation_lines ) fname = _generate_unique_filename(cl, "structure", reserve=_cattrs_use_linecache) diff --git a/tests/test_validation.py b/tests/test_validation.py index eed2cae5..e055b786 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -37,6 +37,24 @@ class Test: ) +def test_external_class_validation(): + """Proper class validation errors are raised when a classes __init__ raises.""" + c = GenConverter(detailed_validation=True) + + @define + class Test: + a: int + b: str = field(validator=in_(["a", "b"])) + c: str + + with pytest.raises(ClassValidationError) as exc: + c.structure({"a": 1, "b": "c", "c": "1"}, Test) + + assert repr(exc.value.exceptions[0]) == repr( + ValueError("'b' must be in ['a', 'b'] (got 'c')") + ) + + def test_list_validation(): """Proper validation errors are raised structuring lists.""" c = GenConverter(detailed_validation=True)