Source code for cattrs.v
"""Cattrs validation."""
from typing import Callable, List, Union
from .errors import (
ClassValidationError,
ForbiddenExtraKeysError,
IterableValidationError,
)
__all__ = ["format_exception", "transform_error"]
[docs]def format_exception(exc: BaseException, type: Union[type, None]) -> str:
"""The default exception formatter, handling the most common exceptions.
The following exceptions are handled specially:
* `KeyErrors` (`required field missing`)
* `ValueErrors` (`invalid value for type, expected <type>` or just `invalid value`)
* `TypeErrors` (`invalid value for type, expected <type>` and a couple special
cases for iterables)
* `cattrs.ForbiddenExtraKeysError`
* some `AttributeErrors` (special cased for structing mappings)
"""
if isinstance(exc, KeyError):
res = "required field missing"
elif isinstance(exc, ValueError):
if type is not None:
tn = type.__name__ if hasattr(type, "__name__") else repr(type)
res = f"invalid value for type, expected {tn}"
else:
res = "invalid value"
elif isinstance(exc, TypeError):
if type is None:
if exc.args[0].endswith("object is not iterable"):
res = "invalid value for type, expected an iterable"
else:
res = f"invalid type ({exc})"
else:
tn = type.__name__ if hasattr(type, "__name__") else repr(type)
res = f"invalid value for type, expected {tn}"
elif isinstance(exc, ForbiddenExtraKeysError):
res = f"extra fields found ({', '.join(exc.extra_fields)})"
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
"object has no attribute 'items'"
):
# This was supposed to be a mapping (and have .items()) but it something else.
res = "expected a mapping"
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
"object has no attribute 'copy'"
):
# This was supposed to be a mapping (and have .copy()) but it something else.
# Used for TypedDicts.
res = "expected a mapping"
else:
res = f"unknown error ({exc})"
return res
[docs]def transform_error(
exc: Union[ClassValidationError, IterableValidationError, BaseException],
path: str = "$",
format_exception: Callable[
[BaseException, Union[type, None]], str
] = format_exception,
) -> List[str]:
"""Transform an exception into a list of error messages.
To get detailed error messages, the exception should be produced by a converter
with `detailed_validation` set.
By default, the error messages are in the form of `{description} @ {path}`.
While traversing the exception and subexceptions, the path is formed:
* by appending `.{field_name}` for fields in classes
* by appending `[{int}]` for indices in iterables, like lists
* by appending `[{str}]` for keys in mappings, like dictionaries
:param exc: The exception to transform into error messages.
:param path: The root path to use.
:param format_exception: A callable to use to transform `Exceptions` into
string descriptions of errors.
.. versionadded:: 23.1.0
"""
errors = []
if isinstance(exc, IterableValidationError):
with_notes, without = exc.group_exceptions()
for exc, note in with_notes:
p = f"{path}[{note.index!r}]"
if isinstance(exc, (ClassValidationError, IterableValidationError)):
errors.extend(transform_error(exc, p, format_exception))
else:
errors.append(f"{format_exception(exc, note.type)} @ {p}")
for exc in without:
errors.append(f"{format_exception(exc, None)} @ {path}")
elif isinstance(exc, ClassValidationError):
with_notes, without = exc.group_exceptions()
for exc, note in with_notes:
p = f"{path}.{note.name}"
if isinstance(exc, (ClassValidationError, IterableValidationError)):
errors.extend(transform_error(exc, p, format_exception))
else:
errors.append(f"{format_exception(exc, note.type)} @ {p}")
for exc in without:
errors.append(f"{format_exception(exc, None)} @ {path}")
else:
errors.append(f"{format_exception(exc, None)} @ {path}")
return errors