From 6910f368491758923160b682e37c3491eb7be441 Mon Sep 17 00:00:00 2001 From: Matan-El Date: Tue, 5 Dec 2023 21:50:46 +0200 Subject: [PATCH 1/2] adding indicative error message to union decoding --- pyrallis/parsers/decoding.py | 31 ++++++++++++++++++++----------- pyrallis/utils.py | 4 ++++ tests/test_decoding.py | 5 +++-- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pyrallis/parsers/decoding.py b/pyrallis/parsers/decoding.py index 3777646..46006e8 100644 --- a/pyrallis/parsers/decoding.py +++ b/pyrallis/parsers/decoding.py @@ -18,7 +18,8 @@ is_enum, ParsingError, format_error, - has_generic_arg + has_generic_arg, + add_tab_to_new_lines ) logger = getLogger(__name__) @@ -195,19 +196,28 @@ def _decode_optional(val: Optional[Any]) -> Optional[T]: return _decode_optional -def try_functions(*funcs: Callable[[Any], T]) -> Callable[[Any], Union[T, Any]]: +def try_decoding_functions(funcs: List[Callable[[Any], T]], + types: List[T]) -> Callable[[Any], Union[T, Any]]: """Tries to use the functions in succession, else returns the same value unchanged.""" - def _try_functions(val: Any) -> Union[T, Any]: - for func in funcs: + assert len(funcs) == len(types), 'Each decoding function must have a unique type.' + + def _try_decoding_functions(val: Any) -> Union[T, Any]: + error_messages: List[str] = [] + for func, t in zip(funcs, types): try: return func(val) - except Exception: + except Exception as e: + error_message = f"-> Class {t.__name__}: {add_tab_to_new_lines(format_error(e))}" + error_messages.append(error_message) continue # If no function worked, raise an exception - raise TypeError(f"No valid parsing for value {val}") + exception_message = f"No valid parsing for value {val}. \nGot parsing errors:" + for error_message in error_messages: + exception_message += f'\n{error_message}' + raise TypeError(add_tab_to_new_lines(exception_message)) - return _try_functions + return _try_decoding_functions def decode_union(*types: Type[T]) -> Callable[[Any], Union[T, Any]]: @@ -221,7 +231,7 @@ def decode_union(*types: Type[T]) -> Callable[[Any], Union[T, Any]]: decode_optional(t) if optional else get_decoding_fn(t) for t in types ] # Try using each of the non-None types, in succession. Worst case, return the value. - return try_functions(*decoding_fns) + return try_decoding_functions(funcs=decoding_fns, types=types) def decode_list(t: Type[T]) -> Callable[[List[Any]], List[T]]: @@ -314,11 +324,10 @@ def no_op(v: T) -> T: def try_constructor(t: Type[T]) -> Callable[[Any], Union[T, Any]]: """ Tries to use the type as a constructor. If that fails, returns the value as-is. """ - return try_functions(lambda val: t(**val), lambda val: t(val)) + funcs = [lambda val: t(**val), lambda val: t(val)] + return try_decoding_functions(funcs=funcs, types=[t, t]) from pathlib import Path decode.register(Path, Path) - - diff --git a/pyrallis/utils.py b/pyrallis/utils.py index 8dc4c26..5b3e094 100644 --- a/pyrallis/utils.py +++ b/pyrallis/utils.py @@ -298,6 +298,10 @@ def format_error(e: Exception): return f'Exception: {e}' +def add_tab_to_new_lines(text: str): + return text.replace('\n', '\n\t') + + def is_generic_arg(arg): try: return arg.__name__ in ['KT', 'VT', 'T'] diff --git a/tests/test_decoding.py b/tests/test_decoding.py index f52564b..e2d69d0 100644 --- a/tests/test_decoding.py +++ b/tests/test_decoding.py @@ -3,6 +3,7 @@ import yaml import json +import toml from pyrallis.utils import PyrallisException from .testutils import * @@ -49,7 +50,7 @@ class SomeClass: new_b = pyrallis.parse(config_class=SomeClass, config_path=tmp_file, args="") assert new_b == b - arguments = shlex.split(f"--config_path {tmp_file}") + arguments = ['--config_path', str(tmp_file)] new_b = pyrallis.parse(config_class=SomeClass, args=arguments) assert new_b == b @@ -98,7 +99,7 @@ class SomeClass: new_b = pyrallis.parse(config_class=SomeClass, config_path=tmp_file, args="") assert new_b == b - arguments = shlex.split(f"--config_path {tmp_file}") + arguments = ['--config_path', str(tmp_file)] new_b = pyrallis.parse(config_class=SomeClass, args=arguments) assert new_b == b From 7a4ea448da168ae0275e7e3bf927e46c3846d2d3 Mon Sep 17 00:00:00 2001 From: Matan-El Date: Tue, 5 Dec 2023 21:50:46 +0200 Subject: [PATCH 2/2] adding indicative error message to union decoding --- pyrallis/parsers/decoding.py | 37 +++++++++++++++++++++++++----------- pyrallis/utils.py | 4 ++++ tests/test_decoding.py | 5 +++-- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pyrallis/parsers/decoding.py b/pyrallis/parsers/decoding.py index 3777646..f65d66f 100644 --- a/pyrallis/parsers/decoding.py +++ b/pyrallis/parsers/decoding.py @@ -18,7 +18,8 @@ is_enum, ParsingError, format_error, - has_generic_arg + has_generic_arg, + add_tab_to_new_lines ) logger = getLogger(__name__) @@ -195,19 +196,31 @@ def _decode_optional(val: Optional[Any]) -> Optional[T]: return _decode_optional -def try_functions(*funcs: Callable[[Any], T]) -> Callable[[Any], Union[T, Any]]: +def try_decoding_functions(funcs: List[Callable[[Any], T]], + types: List[T]) -> Callable[[Any], Union[T, Any]]: """Tries to use the functions in succession, else returns the same value unchanged.""" - def _try_functions(val: Any) -> Union[T, Any]: - for func in funcs: + assert len(funcs) == len(types), 'Each decoding function must have a unique type.' + + def _try_decoding_functions(val: Any) -> Union[T, Any]: + error_messages: List[str] = [] + for func, t in zip(funcs, types): try: return func(val) - except Exception: + except Exception as e: + error_message = f"-> Failed to parse as class {t.__name__}: {add_tab_to_new_lines(format_error(e))}" + error_messages.append(error_message) continue # If no function worked, raise an exception - raise TypeError(f"No valid parsing for value {val}") + class_names = [t.__name__ for t in types] + exception_message = f"Failed to parse value for multiple classes: " \ + f"value={val}, classes={class_names}.\n" \ + f"Got parsing errors:" + for error_message in error_messages: + exception_message += f'\n{error_message}' + raise TypeError(add_tab_to_new_lines(exception_message)) - return _try_functions + return _try_decoding_functions def decode_union(*types: Type[T]) -> Callable[[Any], Union[T, Any]]: @@ -217,11 +230,14 @@ def decode_union(*types: Type[T]) -> Callable[[Any], Union[T, Any]]: while type(None) in types: types.remove(type(None)) + if len(types) == 1: # There is a single not None class + return decode_optional(types[0]) + decoding_fns: List[Callable[[Any], T]] = [ decode_optional(t) if optional else get_decoding_fn(t) for t in types ] # Try using each of the non-None types, in succession. Worst case, return the value. - return try_functions(*decoding_fns) + return try_decoding_functions(funcs=decoding_fns, types=types) def decode_list(t: Type[T]) -> Callable[[List[Any]], List[T]]: @@ -314,11 +330,10 @@ def no_op(v: T) -> T: def try_constructor(t: Type[T]) -> Callable[[Any], Union[T, Any]]: """ Tries to use the type as a constructor. If that fails, returns the value as-is. """ - return try_functions(lambda val: t(**val), lambda val: t(val)) + funcs = [lambda val: t(**val), lambda val: t(val)] + return try_decoding_functions(funcs=funcs, types=[t, t]) from pathlib import Path decode.register(Path, Path) - - diff --git a/pyrallis/utils.py b/pyrallis/utils.py index 8dc4c26..5b3e094 100644 --- a/pyrallis/utils.py +++ b/pyrallis/utils.py @@ -298,6 +298,10 @@ def format_error(e: Exception): return f'Exception: {e}' +def add_tab_to_new_lines(text: str): + return text.replace('\n', '\n\t') + + def is_generic_arg(arg): try: return arg.__name__ in ['KT', 'VT', 'T'] diff --git a/tests/test_decoding.py b/tests/test_decoding.py index f52564b..e2d69d0 100644 --- a/tests/test_decoding.py +++ b/tests/test_decoding.py @@ -3,6 +3,7 @@ import yaml import json +import toml from pyrallis.utils import PyrallisException from .testutils import * @@ -49,7 +50,7 @@ class SomeClass: new_b = pyrallis.parse(config_class=SomeClass, config_path=tmp_file, args="") assert new_b == b - arguments = shlex.split(f"--config_path {tmp_file}") + arguments = ['--config_path', str(tmp_file)] new_b = pyrallis.parse(config_class=SomeClass, args=arguments) assert new_b == b @@ -98,7 +99,7 @@ class SomeClass: new_b = pyrallis.parse(config_class=SomeClass, config_path=tmp_file, args="") assert new_b == b - arguments = shlex.split(f"--config_path {tmp_file}") + arguments = ['--config_path', str(tmp_file)] new_b = pyrallis.parse(config_class=SomeClass, args=arguments) assert new_b == b