Skip to content

Commit

Permalink
Improve error messages (#51)
Browse files Browse the repository at this point in the history
* Improve error messages

* String chars only for string types

* Fix parameter infos

* Better param detection

* error message customization

* Better type errors from `required_field`

* 🔥
  • Loading branch information
lord-haffi authored Jan 14, 2024
1 parent 16f8aa5 commit ed7cb71
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 36 deletions.
21 changes: 16 additions & 5 deletions src/pvframework/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ def format_parameter_infos(
is_required = (
validator.signature.parameters[param_name].default == validator.signature.parameters[param_name].empty
)
param_value = provided_params[param_name].value if is_provided else param.default
if isinstance(param_value, str):
param_value = f"'{param_value}'"
if param_value is None:
param_value = "None"
param_description = (
f"value='{provided_params[param_name].value if is_provided else param.default}', "
f"id='{provided_params[param_name].param_id if param_name in provided_params else 'unprovided'}', "
f"value={param_value}, "
f"id={provided_params[param_name].param_id if param_name in provided_params else 'unprovided'}, "
f"{'required' if is_required else 'optional'}, "
f"{'provided' if is_provided else 'unprovided'}"
)
Expand Down Expand Up @@ -132,10 +137,14 @@ def __init__(
provided_params = validation_manager.info.running_tasks[
validation_manager.info.tasks[mapped_validator]
].current_provided_params
data_set_str = str(data_set)
if len(data_set_str) > 80:
data_set_str = data_set_str[:77] + "..."
message = (
f"{error_id}: {message_detail}\n"
f"\tDataSet: {data_set})\n"
f"{error_id}, {type(cause).__name__}: {message_detail}\n"
f"\tDataSet: {data_set_str}\n"
f"\tError ID: {error_id}\n"
f"\tError type: {type(cause).__name__}\n"
f"\tValidator function: {mapped_validator.name}"
)
if provided_params is not None:
Expand All @@ -145,11 +154,13 @@ def __init__(
start_indent="\t\t",
)
message += f"\n\tParameter information: \n{formatted_param_infos}"
else:
message += "\n\tParameter information: No info"
super().__init__(message)
self.cause = cause
self.data_set = data_set
self.mapped_validator = mapped_validator
self.validator_set = validation_manager
self.validation_manager = validation_manager
self.error_id = error_id
self.message_detail = message_detail
self.provided_params = provided_params
Expand Down
20 changes: 12 additions & 8 deletions src/pvframework/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ async def _dependency_errored(self, current_mapped_validator: MappedValidatorSyn
async def _are_params_ok(
self, mapped_validator: MappedValidatorSyncAsync, params_or_exc: Parameters[DataSetT] | Exception
) -> bool:
self.info.current_provided_params = params_or_exc if not isinstance(params_or_exc, Exception) else None
if isinstance(params_or_exc, Exception):
await self.info.error_handler.catch(
str(params_or_exc),
Expand All @@ -265,18 +266,21 @@ async def _are_params_ok(
custom_error_id=_CustomErrorIDS.PARAM_PROVIDER_ERRORED,
)
return False
try:
self.info.current_provided_params = params_or_exc
for param_name, param in params_or_exc.items():
for param_name, param in params_or_exc.items():
try:
check_type(
param.value,
mapped_validator.validator.signature.parameters[param_name].annotation,
)
except TypeCheckError as error:
await self.info.error_handler.catch(
str(error), error, mapped_validator, self, custom_error_id=_CustomErrorIDS.PARAM_TYPE_MISMATCH
)
return False
except TypeCheckError as error:
await self.info.error_handler.catch(
f"{param.param_id}: {error}",
error,
mapped_validator,
self,
custom_error_id=_CustomErrorIDS.PARAM_TYPE_MISMATCH,
)
return False
return True

async def _execute_async_validator(
Expand Down
2 changes: 1 addition & 1 deletion src/pvframework/mapped_validators/path_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def provide(self, data_set: DataSetT) -> Generator[Parameters[DataSetT] | Except
provided = True
except AttributeError as error:
if param_name in self.validator.required_param_names:
query_error = AttributeError(f"{attr_path} not provided")
query_error = AttributeError(f"{attr_path}: value not provided")
query_error.__cause__ = error
yield query_error
skip = True
Expand Down
4 changes: 2 additions & 2 deletions src/pvframework/mapped_validators/query_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ def _iter_func(data_set: DataSetT) -> Iterator[IteratorReturnTypeWithException]:
sub_el: Any = required_field(parent_el[0], attr_path, Any)
yield sub_el, f"{parent_el[1]}.{attr_path}"
except AttributeError as error:
query_error = AttributeError(f"{parent_el[1]}.{attr_path} not provided")
query_error = AttributeError(f"{parent_el[1]}.{attr_path}: value not provided")
query_error.__cause__ = error
yield query_error
else:
try:
sub_el = required_field(data_set, attr_path, Any)
yield sub_el, attr_path
except AttributeError as error:
query_error = AttributeError(f"{attr_path} not provided")
query_error = AttributeError(f"{attr_path}: value not provided")
query_error.__cause__ = error
yield query_error

Expand Down
27 changes: 17 additions & 10 deletions src/pvframework/utils/frame_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ def validate_email(e_mail: Optional[str] = None):
"""
call_stack = inspect.stack()
# call_stack[0] -> this function
# call_stack[1] -> must be the validator function
# call_stack[2] -> should be either `_execute_sync_validator` or `_execute_async_validator`
# call_stack[...]
# call_stack[i-1] -> must be the validator function
# call_stack[i] -> should be either `_execute_sync_validator` or `_execute_async_validator`
frame_info_candidate = None
for frame_info in call_stack[2:]:
if frame_info.function in ("_execute_sync_validator", "_execute_async_validator"):
frame_info_candidate = frame_info
break
validation_manager: Optional[ValidationManager] = None
try:
validation_manager = call_stack[2].frame.f_locals["self"]
if not isinstance(validation_manager, ValidationManager):
validation_manager = None
except KeyError:
pass
if frame_info_candidate is not None:
try:
validation_manager = frame_info_candidate.frame.f_locals["self"]
if not isinstance(validation_manager, ValidationManager):
validation_manager = None
except KeyError:
pass

if validation_manager is None:
raise RuntimeError(
"You can call this function only directly from inside a function "
"which is executed by the validation framework"
"This function only works if it is called somewhere inside a validator function "
"(must be in the function stack) which is executed by the validation framework"
)

provided_params: Optional[Parameters] = validation_manager.info.current_provided_params
Expand Down
16 changes: 12 additions & 4 deletions src/pvframework/utils/query_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from typing import TYPE_CHECKING, Any, Optional, TypeVar, overload

from typeguard import check_type
from typeguard import TypeCheckError, check_type

if TYPE_CHECKING:
pass
Expand Down Expand Up @@ -33,7 +33,7 @@ def required_field(obj: Any, attribute_path: str, attribute_type: Any) -> Any:
...


def required_field(obj: Any, attribute_path: str, attribute_type: Any) -> Any:
def required_field(obj: Any, attribute_path: str, attribute_type: Any, param_base_path: Optional[str] = None) -> Any:
"""
Tries to query the `obj` with the provided `attribute_path`. If it is not existent,
an AttributeError will be raised.
Expand All @@ -47,6 +47,14 @@ def required_field(obj: Any, attribute_path: str, attribute_type: Any) -> Any:
current_obj = getattr(current_obj, attr_name)
except AttributeError as error:
current_path = ".".join(splitted_path[0 : index + 1])
raise AttributeError(f"'{current_path}' does not exist") from error
check_type(current_obj, attribute_type)
if param_base_path is not None:
current_path = f"{param_base_path}.{current_path}"
raise AttributeError(f"{current_path}: Not found") from error
try:
check_type(current_obj, attribute_type)
except TypeCheckError as error:
current_path = attribute_path
if param_base_path is not None:
current_path = f"{param_base_path}.{attribute_path}"
raise TypeCheckError(f"{current_path}: {error}") from error
return current_obj
12 changes: 6 additions & 6 deletions unittests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ async def test_unprovided_but_required(self):
validation_summary = await validation_manager.validate(dataset_instance)

assert validation_summary.num_errors_total == 1
assert "z.z not provided" in str(validation_summary.all_errors[0])
assert "z.z: value not provided" in str(validation_summary.all_errors[0])

async def test_multiple_validator_registration(self):
global finishing_order
Expand Down Expand Up @@ -452,14 +452,14 @@ async def test_param_info_fail(self):
with pytest.raises(RuntimeError) as error:
param("x")
assert (
"You can call this function only directly from inside a function "
"which is executed by the validation framework" == str(error.value)
"This function only works if it is called somewhere inside a validator function "
"(must be in the function stack) which is executed by the validation framework" == str(error.value)
)
with pytest.raises(RuntimeError) as error:
TestValidation.wrapper_without_self_for_coverage()
assert (
"You can call this function only directly from inside a function "
"which is executed by the validation framework" == str(error.value)
"This function only works if it is called somewhere inside a validator function "
"(must be in the function stack) which is executed by the validation framework" == str(error.value)
)

async def test_error_ids(self):
Expand Down Expand Up @@ -541,7 +541,7 @@ async def test_special_data_set_errors(self):
SpecialDataSet.model_construct(x=special_dataset_instance.x)
)
assert validation_summary.num_errors_total == 1
assert "y not provided" in str(validation_summary.all_errors[0])
assert "y: value not provided" in str(validation_summary.all_errors[0])
assert len(finishing_order) == 3
assert all(el == check_special_data_set_optional for el in finishing_order)

Expand Down

0 comments on commit ed7cb71

Please sign in to comment.