You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

163 line
5.0 KiB

  1. import json
  2. from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Sequence, Tuple, Type, Union
  3. from .json import pydantic_encoder
  4. from .utils import Representation
  5. if TYPE_CHECKING:
  6. from typing_extensions import TypedDict
  7. from .config import BaseConfig
  8. from .types import ModelOrDc
  9. from .typing import ReprArgs
  10. Loc = Tuple[Union[int, str], ...]
  11. class _ErrorDictRequired(TypedDict):
  12. loc: Loc
  13. msg: str
  14. type: str
  15. class ErrorDict(_ErrorDictRequired, total=False):
  16. ctx: Dict[str, Any]
  17. __all__ = 'ErrorWrapper', 'ValidationError'
  18. class ErrorWrapper(Representation):
  19. __slots__ = 'exc', '_loc'
  20. def __init__(self, exc: Exception, loc: Union[str, 'Loc']) -> None:
  21. self.exc = exc
  22. self._loc = loc
  23. def loc_tuple(self) -> 'Loc':
  24. if isinstance(self._loc, tuple):
  25. return self._loc
  26. else:
  27. return (self._loc,)
  28. def __repr_args__(self) -> 'ReprArgs':
  29. return [('exc', self.exc), ('loc', self.loc_tuple())]
  30. # ErrorList is something like Union[List[Union[List[ErrorWrapper], ErrorWrapper]], ErrorWrapper]
  31. # but recursive, therefore just use:
  32. ErrorList = Union[Sequence[Any], ErrorWrapper]
  33. class ValidationError(Representation, ValueError):
  34. __slots__ = 'raw_errors', 'model', '_error_cache'
  35. def __init__(self, errors: Sequence[ErrorList], model: 'ModelOrDc') -> None:
  36. self.raw_errors = errors
  37. self.model = model
  38. self._error_cache: Optional[List['ErrorDict']] = None
  39. def errors(self) -> List['ErrorDict']:
  40. if self._error_cache is None:
  41. try:
  42. config = self.model.__config__ # type: ignore
  43. except AttributeError:
  44. config = self.model.__pydantic_model__.__config__ # type: ignore
  45. self._error_cache = list(flatten_errors(self.raw_errors, config))
  46. return self._error_cache
  47. def json(self, *, indent: Union[None, int, str] = 2) -> str:
  48. return json.dumps(self.errors(), indent=indent, default=pydantic_encoder)
  49. def __str__(self) -> str:
  50. errors = self.errors()
  51. no_errors = len(errors)
  52. return (
  53. f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.model.__name__}\n'
  54. f'{display_errors(errors)}'
  55. )
  56. def __repr_args__(self) -> 'ReprArgs':
  57. return [('model', self.model.__name__), ('errors', self.errors())]
  58. def display_errors(errors: List['ErrorDict']) -> str:
  59. return '\n'.join(f'{_display_error_loc(e)}\n {e["msg"]} ({_display_error_type_and_ctx(e)})' for e in errors)
  60. def _display_error_loc(error: 'ErrorDict') -> str:
  61. return ' -> '.join(str(e) for e in error['loc'])
  62. def _display_error_type_and_ctx(error: 'ErrorDict') -> str:
  63. t = 'type=' + error['type']
  64. ctx = error.get('ctx')
  65. if ctx:
  66. return t + ''.join(f'; {k}={v}' for k, v in ctx.items())
  67. else:
  68. return t
  69. def flatten_errors(
  70. errors: Sequence[Any], config: Type['BaseConfig'], loc: Optional['Loc'] = None
  71. ) -> Generator['ErrorDict', None, None]:
  72. for error in errors:
  73. if isinstance(error, ErrorWrapper):
  74. if loc:
  75. error_loc = loc + error.loc_tuple()
  76. else:
  77. error_loc = error.loc_tuple()
  78. if isinstance(error.exc, ValidationError):
  79. yield from flatten_errors(error.exc.raw_errors, config, error_loc)
  80. else:
  81. yield error_dict(error.exc, config, error_loc)
  82. elif isinstance(error, list):
  83. yield from flatten_errors(error, config, loc=loc)
  84. else:
  85. raise RuntimeError(f'Unknown error object: {error}')
  86. def error_dict(exc: Exception, config: Type['BaseConfig'], loc: 'Loc') -> 'ErrorDict':
  87. type_ = get_exc_type(exc.__class__)
  88. msg_template = config.error_msg_templates.get(type_) or getattr(exc, 'msg_template', None)
  89. ctx = exc.__dict__
  90. if msg_template:
  91. msg = msg_template.format(**ctx)
  92. else:
  93. msg = str(exc)
  94. d: 'ErrorDict' = {'loc': loc, 'msg': msg, 'type': type_}
  95. if ctx:
  96. d['ctx'] = ctx
  97. return d
  98. _EXC_TYPE_CACHE: Dict[Type[Exception], str] = {}
  99. def get_exc_type(cls: Type[Exception]) -> str:
  100. # slightly more efficient than using lru_cache since we don't need to worry about the cache filling up
  101. try:
  102. return _EXC_TYPE_CACHE[cls]
  103. except KeyError:
  104. r = _get_exc_type(cls)
  105. _EXC_TYPE_CACHE[cls] = r
  106. return r
  107. def _get_exc_type(cls: Type[Exception]) -> str:
  108. if issubclass(cls, AssertionError):
  109. return 'assertion_error'
  110. base_name = 'type_error' if issubclass(cls, TypeError) else 'value_error'
  111. if cls in (TypeError, ValueError):
  112. # just TypeError or ValueError, no extra code
  113. return base_name
  114. # if it's not a TypeError or ValueError, we just take the lowercase of the exception name
  115. # no chaining or snake case logic, use "code" for more complex error types.
  116. code = getattr(cls, 'code', None) or cls.__name__.replace('Error', '').lower()
  117. return base_name + '.' + code