mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Error context and message (#183)
* POC of error context and message * Move type errors to the `errors.py` module; Change errors interface a bit * Rename `.as_dict()` to `.dict()` * Fix `PydanticErrorMixin` constructor * Rename `exceptions.py` to `error_wrappers.py` * Do not include nullable `ctx` * Fix tests * Added `int_validator`; Added `IntegerError` * Added `float_validator`; Added `FloatError` * Get rid of `__mro__` in prior of `exc.code` * Removed `min_number_size` and `max_number_size` from config (#174) * Added `NumberMinSizeError` and `NumberMaxSizeError` * Added `NoneIsNotAllowedError` * Added `EnumError` * Added `path_validator`; Added `PathError` * Added `DictError` * Added `ListError` * Added `TupleError` * Added `SetError` * Added `datetime` related errors * Added `bytes` and `str` related errors * Added `SequenceError` * Improved code coverage * Display error context in string representation of validation error * Redefine error message templates using config * Review fixes * Updated changelog
This commit is contained in:
committed by
Samuel Colvin
parent
0698384788
commit
4f4e22ef47
@@ -6,6 +6,9 @@ History
|
||||
v0.9.2 (2018-XX-XX)
|
||||
...................
|
||||
* add ``Config.allow_population_by_alias`` #160, thanks @bendemaree
|
||||
* **breaking change**: new errors format #179
|
||||
* **breaking change**: removed ``Config.min_number_size`` and ``Config.max_number_size`` #183
|
||||
* added error context and ability to redefine error message templates using ``Config.error_msg_templates`` #183
|
||||
|
||||
v0.9.1 (2018-05-10)
|
||||
...................
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# flake8: noqa
|
||||
from .env_settings import BaseSettings
|
||||
from .exceptions import *
|
||||
from .error_wrappers import ValidationError
|
||||
from .errors import *
|
||||
from .fields import Required
|
||||
from .main import BaseConfig, BaseModel, create_model, validator
|
||||
from .parse import Protocol
|
||||
|
||||
@@ -18,6 +18,8 @@ import re
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from typing import Union
|
||||
|
||||
from . import errors
|
||||
|
||||
date_re = re.compile(r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$')
|
||||
|
||||
time_re = re.compile(
|
||||
@@ -96,10 +98,14 @@ def parse_date(value: Union[date, StrIntFloat]) -> date:
|
||||
|
||||
match = date_re.match(value)
|
||||
if not match:
|
||||
raise ValueError('Invalid date format')
|
||||
raise errors.DateError()
|
||||
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
return date(**kw)
|
||||
|
||||
try:
|
||||
return date(**kw)
|
||||
except ValueError as e:
|
||||
raise errors.DateError() from e
|
||||
|
||||
|
||||
def parse_time(value: Union[time, str]) -> time:
|
||||
@@ -116,13 +122,18 @@ def parse_time(value: Union[time, str]) -> time:
|
||||
|
||||
match = time_re.match(value)
|
||||
if not match:
|
||||
raise ValueError('Invalid time format')
|
||||
raise errors.TimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw['microsecond']:
|
||||
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
|
||||
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
return time(**kw)
|
||||
|
||||
try:
|
||||
return time(**kw)
|
||||
except ValueError as e:
|
||||
raise errors.TimeError() from e
|
||||
|
||||
|
||||
def parse_datetime(value: Union[datetime, StrIntFloat]) -> datetime:
|
||||
@@ -144,11 +155,12 @@ def parse_datetime(value: Union[datetime, StrIntFloat]) -> datetime:
|
||||
|
||||
match = datetime_re.match(value)
|
||||
if not match:
|
||||
raise ValueError('Invalid datetime format')
|
||||
raise errors.DateTimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw['microsecond']:
|
||||
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
|
||||
|
||||
tzinfo = kw.pop('tzinfo')
|
||||
if tzinfo == 'Z':
|
||||
tzinfo = timezone.utc
|
||||
@@ -158,9 +170,14 @@ def parse_datetime(value: Union[datetime, StrIntFloat]) -> datetime:
|
||||
if tzinfo[0] == '-':
|
||||
offset = -offset
|
||||
tzinfo = timezone(timedelta(minutes=offset))
|
||||
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw['tzinfo'] = tzinfo
|
||||
return datetime(**kw)
|
||||
|
||||
try:
|
||||
return datetime(**kw)
|
||||
except ValueError as e:
|
||||
raise errors.DateTimeError() from e
|
||||
|
||||
|
||||
def parse_duration(value: StrIntFloat) -> timedelta:
|
||||
@@ -177,13 +194,16 @@ def parse_duration(value: StrIntFloat) -> timedelta:
|
||||
|
||||
match = standard_duration_re.match(value) or iso8601_duration_re.match(value)
|
||||
if not match:
|
||||
raise ValueError('Invalid duration format')
|
||||
raise errors.DurationError()
|
||||
|
||||
kw = match.groupdict()
|
||||
sign = -1 if kw.pop('sign', '+') == '-' else 1
|
||||
if kw.get('microseconds'):
|
||||
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
|
||||
|
||||
if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'):
|
||||
kw['microseconds'] = '-' + kw['microseconds']
|
||||
|
||||
kw = {k: float(v) for k, v in kw.items() if v is not None}
|
||||
|
||||
return sign * timedelta(**kw)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from typing import Iterable, Type
|
||||
|
||||
from .errors import PydanticErrorMixin
|
||||
from .utils import to_snake_case
|
||||
|
||||
__all__ = (
|
||||
'ErrorWrapper',
|
||||
'ValidationError',
|
||||
)
|
||||
|
||||
|
||||
class ErrorWrapper:
|
||||
__slots__ = 'exc', 'loc', 'msg_template'
|
||||
|
||||
def __init__(self, exc, *, loc, config=None):
|
||||
self.exc = exc
|
||||
self.loc = loc if isinstance(loc, tuple) else (loc,)
|
||||
self.msg_template = config.error_msg_templates.get(self.type_) if config else None
|
||||
|
||||
@property
|
||||
def ctx(self):
|
||||
return getattr(self.exc, 'ctx', None)
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
default_msg_template = getattr(self.exc, 'msg_template', None)
|
||||
msg_template = self.msg_template or default_msg_template
|
||||
if msg_template:
|
||||
return msg_template.format(**self.ctx or {})
|
||||
|
||||
return str(self.exc)
|
||||
|
||||
@property
|
||||
def type_(self):
|
||||
return get_exc_type(self.exc)
|
||||
|
||||
def dict(self, *, loc_prefix=None):
|
||||
loc = self.loc if loc_prefix is None else loc_prefix + self.loc
|
||||
|
||||
d = {
|
||||
'loc': loc,
|
||||
'msg': self.msg,
|
||||
'type': self.type_,
|
||||
}
|
||||
|
||||
if self.ctx is not None:
|
||||
d['ctx'] = self.ctx
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
__slots__ = 'errors', 'message'
|
||||
|
||||
def __init__(self, errors):
|
||||
self.errors = errors
|
||||
self.message = 'validation errors'
|
||||
|
||||
super().__init__(self.message)
|
||||
|
||||
@property
|
||||
def display_errors(self):
|
||||
return display_errors(self.flatten_errors())
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message}\n{self.display_errors}'
|
||||
|
||||
def flatten_errors(self):
|
||||
return list(flatten_errors(self.errors))
|
||||
|
||||
def json(self, *, indent=2):
|
||||
return json.dumps(self.flatten_errors(), indent=indent, sort_keys=True)
|
||||
|
||||
|
||||
def display_errors(errors):
|
||||
return '\n'.join(
|
||||
f'{_display_error_loc(e)}\n {e["msg"]} ({_display_error_type_and_ctx(e)})'
|
||||
for e in errors
|
||||
)
|
||||
|
||||
|
||||
def _display_error_loc(error):
|
||||
return ' -> '.join(str(l) for l in error['loc'])
|
||||
|
||||
|
||||
def _display_error_type_and_ctx(error):
|
||||
display = f'type={error["type"]}'
|
||||
|
||||
ctx = error.get('ctx')
|
||||
if ctx:
|
||||
ctx = '; '.join(f'{k}={v}' for k, v in ctx.items())
|
||||
display = f'{display}; {ctx}'
|
||||
|
||||
return display
|
||||
|
||||
|
||||
def flatten_errors(errors, *, loc=None):
|
||||
for error in errors:
|
||||
if isinstance(error, ErrorWrapper):
|
||||
if isinstance(error.exc, ValidationError):
|
||||
yield from flatten_errors(error.exc.errors, loc=error.loc)
|
||||
else:
|
||||
yield error.dict(loc_prefix=loc)
|
||||
elif isinstance(error, list):
|
||||
yield from flatten_errors(error)
|
||||
else:
|
||||
raise RuntimeError(f'Unknown error object: {error}')
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_exc_type(exc: Exception) -> str:
|
||||
cls = type(exc)
|
||||
|
||||
if issubclass(cls, PydanticErrorMixin):
|
||||
return _get_pydantic_exc_type(cls)
|
||||
|
||||
bases = tuple(_get_exc_bases(cls))
|
||||
bases = bases[::-1]
|
||||
|
||||
return to_snake_case('.'.join(bases))
|
||||
|
||||
|
||||
def _get_pydantic_exc_type(exc: Type[PydanticErrorMixin]) -> str:
|
||||
if issubclass(exc, TypeError):
|
||||
type_ = 'type_error'
|
||||
else:
|
||||
type_ = 'value_error'
|
||||
|
||||
return f'{type_}.{exc.code}'
|
||||
|
||||
|
||||
def _get_exc_bases(exc: Type[Exception]) -> Iterable[str]:
|
||||
for b in exc.__mro__: # pragma: no branch
|
||||
if b in (TypeError, ValueError):
|
||||
yield b.__name__
|
||||
break
|
||||
|
||||
yield b.__name__.replace('Error', '')
|
||||
@@ -0,0 +1,218 @@
|
||||
from decimal import Decimal
|
||||
from typing import Union
|
||||
|
||||
|
||||
class PydanticErrorMixin:
|
||||
code: str
|
||||
msg_template: str
|
||||
|
||||
def __init__(self, **ctx) -> None:
|
||||
self.ctx = ctx or None
|
||||
super().__init__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.msg_template.format(**self.ctx or {})
|
||||
|
||||
|
||||
class PydanticTypeError(PydanticErrorMixin, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class PydanticValueError(PydanticErrorMixin, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingError(PydanticValueError):
|
||||
code = 'missing'
|
||||
msg_template = 'field required'
|
||||
|
||||
|
||||
class ExtraError(PydanticValueError):
|
||||
code = 'extra'
|
||||
msg_template = 'extra fields not permitted'
|
||||
|
||||
|
||||
class NoneIsNotAllowedError(PydanticTypeError):
|
||||
code = 'none.not_allowed'
|
||||
msg_template = 'none is not an allow value'
|
||||
|
||||
|
||||
class BytesError(PydanticTypeError):
|
||||
code = 'bytes'
|
||||
msg_template = 'byte type expected'
|
||||
|
||||
|
||||
class DictError(PydanticTypeError):
|
||||
code = 'dict'
|
||||
msg_template = 'value is not a valid dict'
|
||||
|
||||
|
||||
class DSNDriverIsEmptyError(PydanticValueError):
|
||||
code = 'dsn.driver_is_empty'
|
||||
msg_template = '"driver" field may not be empty'
|
||||
|
||||
|
||||
class EmailError(PydanticValueError):
|
||||
code = 'email'
|
||||
msg_template = 'value is not a valid email address'
|
||||
|
||||
|
||||
class EnumError(PydanticTypeError):
|
||||
code = 'enum'
|
||||
msg_template = 'value is not a valid enumeration member'
|
||||
|
||||
|
||||
class IntegerError(PydanticTypeError):
|
||||
code = 'integer'
|
||||
msg_template = 'value is not a valid integer'
|
||||
|
||||
|
||||
class FloatError(PydanticTypeError):
|
||||
code = 'float'
|
||||
msg_template = 'value is not a valid float'
|
||||
|
||||
|
||||
class ListError(PydanticTypeError):
|
||||
code = 'list'
|
||||
msg_template = 'value is not a valid list'
|
||||
|
||||
|
||||
class PathError(PydanticTypeError):
|
||||
code = 'path'
|
||||
msg_template = 'value is not a valid path'
|
||||
|
||||
|
||||
class PyObjectError(PydanticTypeError):
|
||||
code = 'py_object'
|
||||
msg_template = 'ensure this value contains valid import path'
|
||||
|
||||
|
||||
class SequenceError(PydanticTypeError):
|
||||
code = 'sequence'
|
||||
msg_template = 'value is not a valid sequence'
|
||||
|
||||
|
||||
class SetError(PydanticTypeError):
|
||||
code = 'set'
|
||||
msg_template = 'value is not a valid set'
|
||||
|
||||
|
||||
class TupleError(PydanticTypeError):
|
||||
code = 'tuple'
|
||||
msg_template = 'value is not a valid tuple'
|
||||
|
||||
|
||||
class AnyStrMinLengthError(PydanticValueError):
|
||||
code = 'any_str.min_length'
|
||||
msg_template = 'ensure this value has at least {limit_value} characters'
|
||||
|
||||
def __init__(self, *, limit_value: int) -> None:
|
||||
super().__init__(limit_value=limit_value)
|
||||
|
||||
|
||||
class AnyStrMaxLengthError(PydanticValueError):
|
||||
code = 'any_str.max_length'
|
||||
msg_template = 'ensure this value has at most {limit_value} characters'
|
||||
|
||||
def __init__(self, *, limit_value: int) -> None:
|
||||
super().__init__(limit_value=limit_value)
|
||||
|
||||
|
||||
class StrError(PydanticTypeError):
|
||||
code = 'str'
|
||||
msg_template = 'str type expected'
|
||||
|
||||
|
||||
class StrRegexError(PydanticValueError):
|
||||
code = 'str.regex'
|
||||
msg_template = 'string does not match regex "{pattern}"'
|
||||
|
||||
def __init__(self, *, pattern: str) -> None:
|
||||
super().__init__(pattern=pattern)
|
||||
|
||||
|
||||
class NumberMinSizeError(PydanticValueError):
|
||||
code = 'number.min_size'
|
||||
msg_template = 'ensure this value is greater than {limit_value}'
|
||||
|
||||
def __init__(self, *, limit_value: Union[int, float, Decimal]) -> None:
|
||||
super().__init__(limit_value=limit_value)
|
||||
|
||||
|
||||
class NumberMaxSizeError(PydanticValueError):
|
||||
code = 'number.max_size'
|
||||
msg_template = 'ensure this value is less than {limit_value}'
|
||||
|
||||
def __init__(self, *, limit_value: Union[int, float, Decimal]) -> None:
|
||||
super().__init__(limit_value=limit_value)
|
||||
|
||||
|
||||
class DecimalError(PydanticTypeError):
|
||||
code = 'decimal'
|
||||
msg_template = 'value is not a valid decimal'
|
||||
|
||||
|
||||
class DecimalIsNotFiniteError(PydanticValueError):
|
||||
code = 'decimal.not_finite'
|
||||
msg_template = 'value is not a valid decimal'
|
||||
|
||||
|
||||
class DecimalMaxDigitsError(PydanticValueError):
|
||||
code = 'decimal.max_digits'
|
||||
msg_template = 'ensure that there are no more than {max_digits} digits in total'
|
||||
|
||||
def __init__(self, *, max_digits: int) -> None:
|
||||
super().__init__(max_digits=max_digits)
|
||||
|
||||
|
||||
class DecimalMaxPlacesError(PydanticValueError):
|
||||
code = 'decimal.max_places'
|
||||
msg_template = 'ensure that there are no more than {decimal_places} decimal places'
|
||||
|
||||
def __init__(self, *, decimal_places: int) -> None:
|
||||
super().__init__(decimal_places=decimal_places)
|
||||
|
||||
|
||||
class DecimalWholeDigitsError(PydanticValueError):
|
||||
code = 'decimal.whole_digits'
|
||||
msg_template = 'ensure that there are no more than {whole_digits} digits before the decimal point'
|
||||
|
||||
def __init__(self, *, whole_digits: int) -> None:
|
||||
super().__init__(whole_digits=whole_digits)
|
||||
|
||||
|
||||
class DateTimeError(PydanticTypeError):
|
||||
code = 'datetime'
|
||||
msg_template = 'invalid datetime format'
|
||||
|
||||
|
||||
class DateError(PydanticTypeError):
|
||||
code = 'date'
|
||||
msg_template = 'invalid date format'
|
||||
|
||||
|
||||
class TimeError(PydanticTypeError):
|
||||
code = 'time'
|
||||
msg_template = 'invalid time format'
|
||||
|
||||
|
||||
class DurationError(PydanticTypeError):
|
||||
code = 'duration'
|
||||
msg_template = 'invalid duration format'
|
||||
|
||||
|
||||
class UUIDError(PydanticTypeError):
|
||||
code = 'uuid'
|
||||
msg_template = 'value is not a valid uuid'
|
||||
|
||||
|
||||
class UUIDVersionError(PydanticValueError):
|
||||
code = 'uuid.version'
|
||||
msg_template = 'uuid version {required_version} expected'
|
||||
|
||||
def __init__(self, *, required_version: int) -> None:
|
||||
super().__init__(required_version=required_version)
|
||||
@@ -1,97 +0,0 @@
|
||||
import inspect
|
||||
import json
|
||||
|
||||
from .utils import to_snake_case
|
||||
|
||||
__all__ = (
|
||||
'Error',
|
||||
'ValidationError',
|
||||
'ConfigError',
|
||||
'Missing',
|
||||
'Extra',
|
||||
)
|
||||
|
||||
|
||||
class Error:
|
||||
__slots__ = 'exc_info', 'loc'
|
||||
|
||||
def __init__(self, exc, *, loc):
|
||||
self.exc_info = exc
|
||||
self.loc = loc if isinstance(loc, tuple) else (loc,)
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
return str(self.exc_info)
|
||||
|
||||
@property
|
||||
def type_(self):
|
||||
bases = []
|
||||
for b in inspect.getmro(type(self.exc_info)): # pragma: no branch
|
||||
bases.append(b.__name__)
|
||||
if b in (ValueError, TypeError):
|
||||
break
|
||||
|
||||
return to_snake_case('.'.join(bases[::-1]))
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
__slots__ = 'errors', 'message'
|
||||
|
||||
def __init__(self, errors):
|
||||
self.errors = errors
|
||||
self.message = 'validation errors'
|
||||
|
||||
super().__init__(self.message)
|
||||
|
||||
@property
|
||||
def display_errors(self):
|
||||
return display_errors(self.flatten_errors())
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message}\n{self.display_errors}'
|
||||
|
||||
def flatten_errors(self):
|
||||
return list(flatten_errors(self.errors))
|
||||
|
||||
def json(self, *, indent=2):
|
||||
return json.dumps(self.flatten_errors(), indent=indent, sort_keys=True)
|
||||
|
||||
|
||||
class ConfigError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Missing(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Extra(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def display_errors(errors):
|
||||
return '\n'.join(
|
||||
f'{_display_error_loc(e["loc"])}\n {e["msg"]} (type={e["type"]})'
|
||||
for e in errors
|
||||
)
|
||||
|
||||
|
||||
def _display_error_loc(loc):
|
||||
return ' -> '.join(str(l) for l in loc)
|
||||
|
||||
|
||||
def flatten_errors(errors, *, loc=None):
|
||||
for error in errors:
|
||||
if isinstance(error, Error):
|
||||
if isinstance(error.exc_info, ValidationError):
|
||||
yield from flatten_errors(error.exc_info.errors, loc=error.loc)
|
||||
else:
|
||||
yield {
|
||||
'loc': error.loc if loc is None else loc + error.loc,
|
||||
'msg': error.msg,
|
||||
'type': error.type_,
|
||||
}
|
||||
elif isinstance(error, list):
|
||||
yield from flatten_errors(error)
|
||||
else:
|
||||
raise RuntimeError(f'Unknown error object: {error}')
|
||||
+13
-12
@@ -2,9 +2,10 @@ import inspect
|
||||
from enum import IntEnum
|
||||
from typing import Any, Callable, List, Mapping, NamedTuple, Set, Type, Union
|
||||
|
||||
from .exceptions import ConfigError, Error
|
||||
from .error_wrappers import ErrorWrapper
|
||||
from .errors import ConfigError, SequenceError
|
||||
from .utils import display_as_type
|
||||
from .validators import NoneType, find_validators, not_none_validator
|
||||
from .validators import NoneType, dict_validator, find_validators, not_none_validator
|
||||
|
||||
Required: Any = Ellipsis
|
||||
|
||||
@@ -238,10 +239,12 @@ class Field:
|
||||
|
||||
def _validate_sequence(self, v, values, loc, cls):
|
||||
result, errors = [], []
|
||||
|
||||
try:
|
||||
v_iter = enumerate(v)
|
||||
except TypeError as exc:
|
||||
return v, Error(exc, loc=loc)
|
||||
except TypeError:
|
||||
return v, ErrorWrapper(SequenceError(), loc=loc, config=self.model_config)
|
||||
|
||||
for i, v_ in v_iter:
|
||||
v_loc = *loc, i
|
||||
single_result, single_errors = self._validate_singleton(v_, values, v_loc, cls)
|
||||
@@ -249,19 +252,17 @@ class Field:
|
||||
errors.append(single_errors)
|
||||
else:
|
||||
result.append(single_result)
|
||||
|
||||
if errors:
|
||||
return v, errors
|
||||
else:
|
||||
return result, None
|
||||
|
||||
def _validate_mapping(self, v, values, loc, cls):
|
||||
if isinstance(v, dict):
|
||||
v_iter = v
|
||||
else:
|
||||
try:
|
||||
v_iter = dict(v)
|
||||
except TypeError:
|
||||
return v, Error(TypeError(f'value is not a valid dict, got {display_as_type(v)}'), loc=loc)
|
||||
try:
|
||||
v_iter = dict_validator(v)
|
||||
except TypeError as exc:
|
||||
return v, ErrorWrapper(exc, loc=loc, config=self.model_config)
|
||||
|
||||
result, errors = {}, []
|
||||
for k, v_ in v_iter.items():
|
||||
@@ -309,7 +310,7 @@ class Field:
|
||||
# ValidatorSignature.CLS_VALUE_KWARGS
|
||||
v = validator(cls, v, values=values, config=self.model_config, field=self)
|
||||
except (ValueError, TypeError) as exc:
|
||||
return v, Error(exc, loc=loc)
|
||||
return v, ErrorWrapper(exc, loc=loc, config=self.model_config)
|
||||
return v, None
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
+12
-16
@@ -6,7 +6,8 @@ from pathlib import Path
|
||||
from types import FunctionType
|
||||
from typing import Any, Dict, Set, Type, Union
|
||||
|
||||
from .exceptions import ConfigError, Error, Extra, Missing, ValidationError
|
||||
from .error_wrappers import ErrorWrapper, ValidationError
|
||||
from .errors import ConfigError, ExtraError, MissingError
|
||||
from .fields import Field, Validator
|
||||
from .parse import Protocol, load_file, load_str_bytes
|
||||
from .types import StrBytes
|
||||
@@ -18,8 +19,6 @@ class BaseConfig:
|
||||
anystr_strip_whitespace = False
|
||||
min_anystr_length = 0
|
||||
max_anystr_length = 2 ** 16
|
||||
min_number_size = -2 ** 64
|
||||
max_number_size = 2 ** 64
|
||||
validate_all = False
|
||||
ignore_extra = True
|
||||
allow_extra = False
|
||||
@@ -28,6 +27,7 @@ class BaseConfig:
|
||||
use_enum_values = False
|
||||
fields = {}
|
||||
validate_assignment = False
|
||||
error_msg_templates: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def get_field_config(cls, name):
|
||||
@@ -137,10 +137,6 @@ class MetaModel(ABCMeta):
|
||||
return super().__new__(mcs, name, bases, new_namespace)
|
||||
|
||||
|
||||
MISSING = Missing('field required')
|
||||
EXTRA = Extra('extra fields not permitted')
|
||||
|
||||
|
||||
class BaseModel(metaclass=MetaModel):
|
||||
# populated by the metaclass, defined here to help IDEs only
|
||||
__fields__ = {}
|
||||
@@ -191,7 +187,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
def parse_obj(cls, obj):
|
||||
if not isinstance(obj, dict):
|
||||
exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}')
|
||||
raise ValidationError([Error(exc, loc='__obj__')])
|
||||
raise ValidationError([ErrorWrapper(exc, loc='__obj__')])
|
||||
return cls(**obj)
|
||||
|
||||
@classmethod
|
||||
@@ -204,7 +200,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
obj = load_str_bytes(b, proto=proto, content_type=content_type, encoding=encoding,
|
||||
allow_pickle=allow_pickle)
|
||||
except (ValueError, TypeError, UnicodeDecodeError) as e:
|
||||
raise ValidationError([Error(e, loc='__obj__')])
|
||||
raise ValidationError([ErrorWrapper(e, loc='__obj__')])
|
||||
return cls.parse_obj(obj)
|
||||
|
||||
@classmethod
|
||||
@@ -265,22 +261,22 @@ class BaseModel(metaclass=MetaModel):
|
||||
errors = []
|
||||
|
||||
for name, field in self.__fields__.items():
|
||||
value = input_data.get(field.alias, MISSING)
|
||||
if value is MISSING and self.__config__.allow_population_by_alias and field.alt_alias:
|
||||
value = input_data.get(field.name, MISSING)
|
||||
value = input_data.get(field.alias, ...)
|
||||
if value is ... and self.__config__.allow_population_by_alias and field.alt_alias:
|
||||
value = input_data.get(field.name, ...)
|
||||
|
||||
if value is MISSING:
|
||||
if value is ...:
|
||||
if self.__config__.validate_all or field.validate_always:
|
||||
value = field.default
|
||||
else:
|
||||
if field.required:
|
||||
errors.append(Error(MISSING, loc=field.alias))
|
||||
errors.append(ErrorWrapper(MissingError(), loc=field.alias, config=self.__config__))
|
||||
else:
|
||||
values[name] = field.default
|
||||
continue
|
||||
|
||||
v_, errors_ = field.validate(value, values, loc=field.alias, cls=self.__class__)
|
||||
if isinstance(errors_, Error):
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
@@ -296,7 +292,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
else:
|
||||
# config.ignore_extra is False
|
||||
for field in sorted(extra):
|
||||
errors.append(Error(EXTRA, loc=field))
|
||||
errors.append(ErrorWrapper(ExtraError(), loc=field, config=self.__config__))
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
+17
-13
@@ -3,9 +3,10 @@ from decimal import Decimal
|
||||
from typing import Optional, Pattern, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from . import errors
|
||||
from .utils import import_string, make_dsn, validate_email
|
||||
from .validators import (anystr_length_validator, anystr_strip_whitespace, decimal_validator, not_none_validator,
|
||||
number_size_validator, str_validator)
|
||||
from .validators import (anystr_length_validator, anystr_strip_whitespace, decimal_validator, float_validator,
|
||||
int_validator, not_none_validator, number_size_validator, str_validator)
|
||||
|
||||
try:
|
||||
import email_validator
|
||||
@@ -54,7 +55,7 @@ class StrictStr(str):
|
||||
@classmethod
|
||||
def validate(cls, v):
|
||||
if not isinstance(v, str):
|
||||
raise ValueError(f'strict string: str expected not {type(v)}')
|
||||
raise errors.StrError()
|
||||
return v
|
||||
|
||||
|
||||
@@ -80,7 +81,7 @@ class ConstrainedStr(str):
|
||||
|
||||
if cls.regex:
|
||||
if not cls.regex.match(value):
|
||||
raise ValueError(f'string does not match regex "{cls.regex.pattern}"')
|
||||
raise errors.StrRegexError(pattern=cls.regex.pattern)
|
||||
|
||||
return value
|
||||
|
||||
@@ -91,6 +92,7 @@ class EmailStr(str):
|
||||
# included here and below so the error happens straight away
|
||||
if email_validator is None:
|
||||
raise ImportError('email-validator is not installed, run `pip install pydantic[email]`')
|
||||
|
||||
yield str_validator
|
||||
yield cls.validate
|
||||
|
||||
@@ -110,6 +112,7 @@ class NameEmail:
|
||||
def get_validators(cls):
|
||||
if email_validator is None:
|
||||
raise ImportError('email-validator is not installed, run `pip install pydantic[email]`')
|
||||
|
||||
yield str_validator
|
||||
yield cls.validate
|
||||
|
||||
@@ -149,8 +152,7 @@ class PyObject:
|
||||
try:
|
||||
return import_string(value)
|
||||
except ImportError as e:
|
||||
# errors must be TypeError or ValueError
|
||||
raise ValueError(str(e)) from e
|
||||
raise errors.PyObjectError() from e
|
||||
|
||||
|
||||
class DSN(str):
|
||||
@@ -167,9 +169,11 @@ class DSN(str):
|
||||
def validate(cls, value, values, **kwarg):
|
||||
if value:
|
||||
return value
|
||||
|
||||
kwargs = {f: values.get(cls.prefix + f) for f in cls.fields}
|
||||
if kwargs['driver'] is None:
|
||||
raise ValueError(f'"{cls.prefix}driver" field may not be missing or None')
|
||||
raise errors.DSNDriverIsEmptyError()
|
||||
|
||||
return make_dsn(**kwargs)
|
||||
|
||||
|
||||
@@ -179,7 +183,7 @@ class ConstrainedInt(int):
|
||||
|
||||
@classmethod
|
||||
def get_validators(cls):
|
||||
yield int
|
||||
yield int_validator
|
||||
yield number_size_validator
|
||||
|
||||
|
||||
@@ -203,7 +207,7 @@ class ConstrainedFloat(float):
|
||||
|
||||
@classmethod
|
||||
def get_validators(cls):
|
||||
yield float
|
||||
yield float_validator
|
||||
yield number_size_validator
|
||||
|
||||
|
||||
@@ -238,7 +242,7 @@ class ConstrainedDecimal(Decimal):
|
||||
def validate(cls, value: Decimal) -> Decimal:
|
||||
digit_tuple, exponent = value.as_tuple()[1:]
|
||||
if exponent in {'F', 'n', 'N'}:
|
||||
raise ValueError(f'value is not a valid decimal, got {value}')
|
||||
raise errors.DecimalIsNotFiniteError()
|
||||
|
||||
if exponent >= 0:
|
||||
# A positive exponent adds that many trailing zeros.
|
||||
@@ -258,15 +262,15 @@ class ConstrainedDecimal(Decimal):
|
||||
whole_digits = digits - decimals
|
||||
|
||||
if cls.max_digits is not None and digits > cls.max_digits:
|
||||
raise ValueError(f'ensure that there are no more than {cls.max_digits} digits in total')
|
||||
raise errors.DecimalMaxDigitsError(max_digits=cls.max_digits)
|
||||
|
||||
if cls.decimal_places is not None and decimals > cls.decimal_places:
|
||||
raise ValueError(f'ensure that there are no more than {cls.decimal_places} decimal places')
|
||||
raise errors.DecimalMaxPlacesError(decimal_places=cls.decimal_places)
|
||||
|
||||
if cls.max_digits is not None and cls.decimal_places is not None:
|
||||
expected = cls.max_digits - cls.decimal_places
|
||||
if whole_digits > expected:
|
||||
raise ValueError(f'ensure that there are no more than {expected} digits before the decimal point')
|
||||
raise errors.DecimalWholeDigitsError(whole_digits=expected)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
+10
-1
@@ -2,11 +2,14 @@ import re
|
||||
from importlib import import_module
|
||||
from typing import Tuple, _TypingBase
|
||||
|
||||
from . import errors
|
||||
|
||||
try:
|
||||
import email_validator
|
||||
except ImportError:
|
||||
email_validator = None
|
||||
|
||||
|
||||
PRETTY_REGEX = re.compile(r'([\w ]*?) *<(.*)> *')
|
||||
|
||||
|
||||
@@ -29,8 +32,14 @@ def validate_email(value) -> Tuple[str, str]:
|
||||
name, value = m.groups()
|
||||
else:
|
||||
name = None
|
||||
|
||||
email = value.strip()
|
||||
email_validator.validate_email(email, check_deliverability=False)
|
||||
|
||||
try:
|
||||
email_validator.validate_email(email, check_deliverability=False)
|
||||
except email_validator.EmailNotValidError as e:
|
||||
raise errors.EmailError() from e
|
||||
|
||||
return name or email[:email.index('@')], email.lower()
|
||||
|
||||
|
||||
|
||||
+112
-36
@@ -6,8 +6,8 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from . import errors
|
||||
from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
|
||||
from .exceptions import ConfigError
|
||||
from .utils import display_as_type
|
||||
|
||||
NoneType = type(None)
|
||||
@@ -15,7 +15,7 @@ NoneType = type(None)
|
||||
|
||||
def not_none_validator(v):
|
||||
if v is None:
|
||||
raise TypeError('None is not an allow value')
|
||||
raise errors.NoneIsNotAllowedError()
|
||||
return v
|
||||
|
||||
|
||||
@@ -28,13 +28,20 @@ def str_validator(v) -> str:
|
||||
# is there anything else we want to add here? If you think so, create an issue.
|
||||
return str(v)
|
||||
else:
|
||||
raise TypeError(f'str or byte type expected not {display_as_type(v)}')
|
||||
raise errors.StrError()
|
||||
|
||||
|
||||
def bytes_validator(v) -> bytes:
|
||||
if isinstance(v, (bytes, NoneType)):
|
||||
if isinstance(v, bytes):
|
||||
return v
|
||||
return str_validator(v).encode()
|
||||
elif isinstance(v, bytearray):
|
||||
return bytes(v)
|
||||
elif isinstance(v, str):
|
||||
return v.encode('utf-8')
|
||||
elif isinstance(v, (float, int, Decimal)):
|
||||
return str(v).encode('utf-8')
|
||||
else:
|
||||
raise errors.BytesError()
|
||||
|
||||
|
||||
BOOL_STRINGS = {
|
||||
@@ -55,14 +62,36 @@ def bool_validator(v) -> bool:
|
||||
return bool(v)
|
||||
|
||||
|
||||
def number_size_validator(v, field, config, **kwargs):
|
||||
min_size = getattr(field.type_, 'gt', config.min_number_size)
|
||||
if min_size is not None and v <= min_size:
|
||||
raise ValueError(f'size less than minimum allowed: {min_size}')
|
||||
def int_validator(v) -> int:
|
||||
if isinstance(v, int):
|
||||
return v
|
||||
|
||||
max_size = getattr(field.type_, 'lt', config.max_number_size)
|
||||
if max_size is not None and v >= max_size:
|
||||
raise ValueError(f'size greater than maximum allowed: {max_size}')
|
||||
try:
|
||||
v = int(v)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise errors.IntegerError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def float_validator(v) -> float:
|
||||
if isinstance(v, float):
|
||||
return v
|
||||
|
||||
try:
|
||||
v = float(v)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise errors.FloatError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def number_size_validator(v, field, config, **kwargs):
|
||||
if field.type_.gt is not None and v < field.type_.gt:
|
||||
raise errors.NumberMinSizeError(limit_value=field.type_.gt)
|
||||
|
||||
if field.type_.lt is not None and v > field.type_.lt:
|
||||
raise errors.NumberMaxSizeError(limit_value=field.type_.lt)
|
||||
|
||||
return v
|
||||
|
||||
@@ -72,11 +101,11 @@ def anystr_length_validator(v, field, config, **kwargs):
|
||||
|
||||
min_length = getattr(field.type_, 'min_length', config.min_anystr_length)
|
||||
if min_length is not None and v_len < min_length:
|
||||
raise ValueError(f'length less than minimum allowed: {min_length}')
|
||||
raise errors.AnyStrMinLengthError(limit_value=min_length)
|
||||
|
||||
max_length = getattr(field.type_, 'max_length', config.max_anystr_length)
|
||||
if max_length is not None and v_len > max_length:
|
||||
raise ValueError(f'length greater than maximum allowed: {max_length}')
|
||||
raise errors.AnyStrMaxLengthError(limit_value=max_length)
|
||||
|
||||
return v
|
||||
|
||||
@@ -92,52 +121,87 @@ def anystr_strip_whitespace(v, field, config, **kwargs):
|
||||
def ordered_dict_validator(v) -> OrderedDict:
|
||||
if isinstance(v, OrderedDict):
|
||||
return v
|
||||
return OrderedDict(v)
|
||||
|
||||
try:
|
||||
v = OrderedDict(v)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise errors.DictError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def dict_validator(v) -> dict:
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
|
||||
try:
|
||||
return dict(v)
|
||||
except TypeError as e:
|
||||
raise TypeError(f'value is not a valid dict, got {display_as_type(v)}') from e
|
||||
v = dict(v)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise errors.DictError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def list_validator(v) -> list:
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return list(v)
|
||||
|
||||
try:
|
||||
v = list(v)
|
||||
except TypeError as e:
|
||||
raise errors.ListError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def tuple_validator(v) -> tuple:
|
||||
if isinstance(v, tuple):
|
||||
return v
|
||||
return tuple(v)
|
||||
|
||||
try:
|
||||
v = tuple(v)
|
||||
except TypeError as e:
|
||||
raise errors.TupleError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def set_validator(v) -> set:
|
||||
if isinstance(v, set):
|
||||
return v
|
||||
return set(v)
|
||||
|
||||
try:
|
||||
v = set(v)
|
||||
except TypeError as e:
|
||||
raise errors.SetError() from e
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def enum_validator(v, field, config, **kwargs) -> Enum:
|
||||
enum_v = field.type_(v)
|
||||
try:
|
||||
enum_v = field.type_(v)
|
||||
except ValueError as e:
|
||||
raise errors.EnumError() from e
|
||||
|
||||
return enum_v.value if config.use_enum_values else enum_v
|
||||
|
||||
|
||||
def uuid_validator(v, field, config, **kwargs) -> UUID:
|
||||
if isinstance(v, str):
|
||||
v = UUID(v)
|
||||
elif isinstance(v, (bytes, bytearray)):
|
||||
v = UUID(v.decode())
|
||||
elif not isinstance(v, UUID):
|
||||
raise ValueError(f'str, byte or native UUID type expected not {type(v)}')
|
||||
try:
|
||||
if isinstance(v, str):
|
||||
v = UUID(v)
|
||||
elif isinstance(v, (bytes, bytearray)):
|
||||
v = UUID(v.decode())
|
||||
except ValueError as e:
|
||||
raise errors.UUIDError() from e
|
||||
|
||||
if not isinstance(v, UUID):
|
||||
raise errors.UUIDError()
|
||||
|
||||
required_version = getattr(field.type_, '_required_version', None)
|
||||
if required_version and v.version != required_version:
|
||||
raise ValueError(f'uuid version {required_version} expected, not {v.version}')
|
||||
raise errors.UUIDVersionError(required_version=required_version)
|
||||
|
||||
return v
|
||||
|
||||
@@ -153,10 +217,22 @@ def decimal_validator(v) -> Decimal:
|
||||
try:
|
||||
v = Decimal(v)
|
||||
except DecimalException as e:
|
||||
raise TypeError(f'value is not a valid decimal, got {display_as_type(v)}') from e
|
||||
raise errors.DecimalError() from e
|
||||
|
||||
if not v.is_finite():
|
||||
raise TypeError(f'value is not a valid decimal, got {display_as_type(v)}')
|
||||
raise errors.DecimalIsNotFiniteError()
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def path_validator(v) -> Path:
|
||||
if isinstance(v, Path):
|
||||
return v
|
||||
|
||||
try:
|
||||
v = Path(v)
|
||||
except TypeError as e:
|
||||
raise errors.PathError() from e
|
||||
|
||||
return v
|
||||
|
||||
@@ -169,10 +245,10 @@ _VALIDATORS = [
|
||||
(bytes, [not_none_validator, bytes_validator, anystr_strip_whitespace, anystr_length_validator]),
|
||||
|
||||
(bool, [bool_validator]),
|
||||
(int, [int, number_size_validator]),
|
||||
(float, [float, number_size_validator]),
|
||||
(int, [int_validator]),
|
||||
(float, [float_validator]),
|
||||
|
||||
(Path, [Path]),
|
||||
(Path, [path_validator]),
|
||||
|
||||
(datetime, [parse_datetime]),
|
||||
(date, [parse_date]),
|
||||
@@ -197,5 +273,5 @@ def find_validators(type_):
|
||||
if issubclass(type_, val_type):
|
||||
return validators
|
||||
except TypeError as e:
|
||||
raise TypeError(f'error checking inheritance of {type_!r} (type: {display_as_type(type_)})') from e
|
||||
raise ConfigError(f'no validator found for {type_}')
|
||||
raise RuntimeError(f'error checking inheritance of {type_!r} (type: {display_as_type(type_)})') from e
|
||||
raise errors.ConfigError(f'no validator found for {type_}')
|
||||
|
||||
+39
-39
@@ -31,13 +31,13 @@ def test_str_bytes():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -87,13 +87,13 @@ def test_union_int_str():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'int() argument must be a string, a bytes-like object or a number, not \'NoneType\'',
|
||||
'type': 'type_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -121,13 +121,13 @@ def test_typed_list():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v', 1),
|
||||
'msg': 'invalid literal for int() with base 10: \'x\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': ('v', 2),
|
||||
'msg': 'invalid literal for int() with base 10: \'y\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -136,8 +136,8 @@ def test_typed_list():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': '\'int\' object is not iterable',
|
||||
'type': 'type_error',
|
||||
'msg': 'value is not a valid sequence',
|
||||
'type': 'type_error.sequence',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -154,8 +154,8 @@ def test_typed_set():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v', 1),
|
||||
'msg': 'invalid literal for int() with base 10: \'x\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -185,8 +185,8 @@ def test_typed_dict(value, result):
|
||||
[
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid dict, got int',
|
||||
'type': 'type_error',
|
||||
'msg': 'value is not a valid dict',
|
||||
'type': 'type_error.dict',
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -195,8 +195,8 @@ def test_typed_dict(value, result):
|
||||
[
|
||||
{
|
||||
'loc': ('v', 'a'),
|
||||
'msg': 'invalid literal for int() with base 10: \'b\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -205,8 +205,8 @@ def test_typed_dict(value, result):
|
||||
[
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid dict, got list',
|
||||
'type': 'type_error',
|
||||
'msg': 'value is not a valid dict',
|
||||
'type': 'type_error.dict',
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -231,8 +231,8 @@ def test_dict_key_error():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v', '__key__'),
|
||||
'msg': 'invalid literal for int() with base 10: \'foo\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -279,8 +279,8 @@ def test_recursive_list():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v', 0),
|
||||
'msg': 'dictionary update sequence element #0 has length 1; 2 is required',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid dict',
|
||||
'type': 'type_error.dict',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -315,13 +315,13 @@ def test_list_unions():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v', 2),
|
||||
'msg': 'int() argument must be a string, a bytes-like object or a number, not \'NoneType\'',
|
||||
'type': 'type_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': ('v', 2),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -390,8 +390,8 @@ def test_alias_error():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('_a',),
|
||||
'msg': 'invalid literal for int() with base 10: \'foo\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -445,7 +445,7 @@ def test_inheritance():
|
||||
|
||||
|
||||
def test_invalid_type():
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
class Model(BaseModel):
|
||||
x: 43 = 123
|
||||
assert "error checking inheritance of 43 (type: int)" in str(exc_info)
|
||||
@@ -475,8 +475,8 @@ def test_valid_string_types(value, expected):
|
||||
[
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'str or byte type expected not dict',
|
||||
'type': 'type_error',
|
||||
'msg': 'str type expected',
|
||||
'type': 'type_error.str',
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -485,8 +485,8 @@ def test_valid_string_types(value, expected):
|
||||
[
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'str or byte type expected not list',
|
||||
'type': 'type_error',
|
||||
'msg': 'str type expected',
|
||||
'type': 'type_error.str',
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -550,8 +550,8 @@ def test_string_none():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel, ConfigError, ValidationError, create_model, validator
|
||||
from pydantic import BaseModel, ValidationError, create_model, errors, validator
|
||||
|
||||
|
||||
def test_create_model():
|
||||
@@ -31,12 +31,12 @@ def test_invalid_name():
|
||||
|
||||
|
||||
def test_field_wrong_tuple():
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(errors.ConfigError):
|
||||
create_model('FooModel', foo=(1, 2, 3))
|
||||
|
||||
|
||||
def test_config_and_base():
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(errors.ConfigError):
|
||||
create_model('FooModel', __config__=BaseModel.Config, __base__=BaseModel)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from datetime import date, datetime, time, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import errors
|
||||
from pydantic.datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
|
||||
|
||||
|
||||
@@ -28,12 +29,12 @@ def create_tz(minutes):
|
||||
('2012-4-9', date(2012, 4, 9)),
|
||||
(date(2012, 4, 9), date(2012, 4, 9)),
|
||||
# Invalid inputs
|
||||
('x20120423', ValueError),
|
||||
('2012-04-56', ValueError),
|
||||
('x20120423', errors.DateError),
|
||||
('2012-04-56', errors.DateError),
|
||||
])
|
||||
def test_date_parsing(value, result):
|
||||
if result == ValueError:
|
||||
with pytest.raises(ValueError):
|
||||
if result == errors.DateError:
|
||||
with pytest.raises(errors.DateError):
|
||||
parse_date(value)
|
||||
else:
|
||||
assert parse_date(value) == result
|
||||
@@ -47,12 +48,12 @@ def test_date_parsing(value, result):
|
||||
('4:8:16', time(4, 8, 16)),
|
||||
(time(4, 8, 16), time(4, 8, 16)),
|
||||
# Invalid inputs
|
||||
('091500', ValueError),
|
||||
('09:15:90', ValueError),
|
||||
('091500', errors.TimeError),
|
||||
('09:15:90', errors.TimeError),
|
||||
])
|
||||
def test_time_parsing(value, result):
|
||||
if result == ValueError:
|
||||
with pytest.raises(ValueError):
|
||||
if result == errors.TimeError:
|
||||
with pytest.raises(errors.TimeError):
|
||||
parse_time(value)
|
||||
else:
|
||||
assert parse_time(value) == result
|
||||
@@ -79,12 +80,12 @@ def test_time_parsing(value, result):
|
||||
(datetime(2017, 5, 5), datetime(2017, 5, 5)),
|
||||
(0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)),
|
||||
# Invalid inputs
|
||||
('x20120423091500', ValueError),
|
||||
('2012-04-56T09:15:90', ValueError),
|
||||
('x20120423091500', errors.DateTimeError),
|
||||
('2012-04-56T09:15:90', errors.DateTimeError),
|
||||
])
|
||||
def test_datetime_parsing(value, result):
|
||||
if result == ValueError:
|
||||
with pytest.raises(ValueError):
|
||||
if result == errors.DateTimeError:
|
||||
with pytest.raises(errors.DateTimeError):
|
||||
parse_datetime(value)
|
||||
else:
|
||||
assert parse_datetime(value) == result
|
||||
@@ -132,9 +133,9 @@ def test_parse_python_format(delta):
|
||||
('-1:15:30', timedelta(hours=-1, minutes=15, seconds=30)),
|
||||
('-30.1', timedelta(seconds=-30, milliseconds=-100)),
|
||||
# iso_8601
|
||||
('P4Y', ValueError),
|
||||
('P4M', ValueError),
|
||||
('P4W', ValueError),
|
||||
('P4Y', errors.DurationError),
|
||||
('P4M', errors.DurationError),
|
||||
('P4W', errors.DurationError),
|
||||
('P4D', timedelta(days=4)),
|
||||
('P0.5D', timedelta(hours=12)),
|
||||
('PT5H', timedelta(hours=5)),
|
||||
@@ -143,8 +144,8 @@ def test_parse_python_format(delta):
|
||||
('PT0.000005S', timedelta(microseconds=5)),
|
||||
])
|
||||
def test_parse_durations(value, result):
|
||||
if result == ValueError:
|
||||
with pytest.raises(ValueError):
|
||||
if result == errors.DurationError:
|
||||
with pytest.raises(errors.DurationError):
|
||||
parse_duration(value)
|
||||
else:
|
||||
assert parse_duration(value) == result
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
from typing import Dict, List, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import UUID1, BaseModel, conint, errors
|
||||
from pydantic.error_wrappers import ValidationError, flatten_errors, get_exc_type
|
||||
|
||||
|
||||
@pytest.mark.parametrize('result,expected', (
|
||||
(
|
||||
'display_errors',
|
||||
"""\
|
||||
a
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
b -> x
|
||||
field required (type=value_error.missing)
|
||||
b -> z
|
||||
field required (type=value_error.missing)
|
||||
c -> 0 -> x
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
d
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
d
|
||||
value is not a valid uuid (type=type_error.uuid)
|
||||
e -> __key__
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
f -> 0
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
f -> 0
|
||||
none is not an allow value (type=type_error.none.not_allowed)
|
||||
g
|
||||
uuid version 1 expected (type=value_error.uuid.version; required_version=1)
|
||||
h
|
||||
yet another error message template 42 (type=value_error.number.min_size; limit_value=42)""",
|
||||
),
|
||||
(
|
||||
'flatten_errors',
|
||||
[
|
||||
{
|
||||
'loc': (
|
||||
'a',
|
||||
),
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'b',
|
||||
'x',
|
||||
),
|
||||
'msg': 'field required',
|
||||
'type': 'value_error.missing',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'b',
|
||||
'z',
|
||||
),
|
||||
'msg': 'field required',
|
||||
'type': 'value_error.missing',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'c',
|
||||
0,
|
||||
'x',
|
||||
),
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'd',
|
||||
),
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'd',
|
||||
),
|
||||
'msg': 'value is not a valid uuid',
|
||||
'type': 'type_error.uuid',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'e',
|
||||
'__key__',
|
||||
),
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'f',
|
||||
0,
|
||||
),
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'f',
|
||||
0,
|
||||
),
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'g',
|
||||
),
|
||||
'msg': 'uuid version 1 expected',
|
||||
'type': 'value_error.uuid.version',
|
||||
'ctx': {
|
||||
'required_version': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'h',
|
||||
),
|
||||
'msg': 'yet another error message template 42',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': 42,
|
||||
},
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
'json',
|
||||
"""\
|
||||
[
|
||||
{
|
||||
"loc": [
|
||||
"a"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"b",
|
||||
"x"
|
||||
],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"b",
|
||||
"z"
|
||||
],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"c",
|
||||
0,
|
||||
"x"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"d"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"d"
|
||||
],
|
||||
"msg": "value is not a valid uuid",
|
||||
"type": "type_error.uuid"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"e",
|
||||
"__key__"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"f",
|
||||
0
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"f",
|
||||
0
|
||||
],
|
||||
"msg": "none is not an allow value",
|
||||
"type": "type_error.none.not_allowed"
|
||||
},
|
||||
{
|
||||
"ctx": {
|
||||
"required_version": 1
|
||||
},
|
||||
"loc": [
|
||||
"g"
|
||||
],
|
||||
"msg": "uuid version 1 expected",
|
||||
"type": "value_error.uuid.version"
|
||||
},
|
||||
{
|
||||
"ctx": {
|
||||
"limit_value": 42
|
||||
},
|
||||
"loc": [
|
||||
"h"
|
||||
],
|
||||
"msg": "yet another error message template 42",
|
||||
"type": "value_error.number.min_size"
|
||||
}
|
||||
]"""
|
||||
),
|
||||
(
|
||||
'__str__',
|
||||
"""\
|
||||
validation errors
|
||||
a
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
b -> x
|
||||
field required (type=value_error.missing)
|
||||
b -> z
|
||||
field required (type=value_error.missing)
|
||||
c -> 0 -> x
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
d
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
d
|
||||
value is not a valid uuid (type=type_error.uuid)
|
||||
e -> __key__
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
f -> 0
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
f -> 0
|
||||
none is not an allow value (type=type_error.none.not_allowed)
|
||||
g
|
||||
uuid version 1 expected (type=value_error.uuid.version; required_version=1)
|
||||
h
|
||||
yet another error message template 42 (type=value_error.number.min_size; limit_value=42)"""
|
||||
),
|
||||
))
|
||||
def test_validation_error(result, expected):
|
||||
class SubModel(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
z: str
|
||||
|
||||
class Model(BaseModel):
|
||||
a: int
|
||||
b: SubModel
|
||||
c: List[SubModel]
|
||||
d: Union[int, UUID]
|
||||
e: Dict[int, str]
|
||||
f: List[Union[int, str]]
|
||||
g: UUID1
|
||||
h: conint(gt=42)
|
||||
|
||||
class Config:
|
||||
error_msg_templates = {
|
||||
'value_error.number.min_size': 'yet another error message template {limit_value}',
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model.parse_obj({
|
||||
'a': 'not_int',
|
||||
'b': {
|
||||
'y': 42,
|
||||
},
|
||||
'c': [
|
||||
{
|
||||
'x': 'not_int',
|
||||
'y': 42,
|
||||
'z': 'string',
|
||||
},
|
||||
],
|
||||
'd': 'string',
|
||||
'e': {
|
||||
'not_int': 'string',
|
||||
},
|
||||
'f': [
|
||||
None,
|
||||
],
|
||||
'g': uuid4(),
|
||||
'h': 21,
|
||||
})
|
||||
|
||||
result = getattr(exc_info.value, result)
|
||||
if callable(result):
|
||||
result = result()
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_flatten_errors_unknown_error_object():
|
||||
with pytest.raises(RuntimeError):
|
||||
list(flatten_errors([object]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('exc,type_', (
|
||||
(TypeError(), 'type_error'),
|
||||
(ValueError(), 'value_error'),
|
||||
(errors.DecimalIsNotFiniteError(), 'value_error.decimal.not_finite'),
|
||||
))
|
||||
def test_get_exc_type(exc, type_):
|
||||
if isinstance(type_, str):
|
||||
assert get_exc_type(exc) == type_
|
||||
else:
|
||||
with pytest.raises(type_) as exc_info:
|
||||
get_exc_type(exc)
|
||||
assert isinstance(exc_info.value, type_)
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from pydantic import PydanticTypeError
|
||||
|
||||
|
||||
def test_pydantic_error():
|
||||
class TestError(PydanticTypeError):
|
||||
code = 'test_code'
|
||||
msg_template = 'test message template "{test_ctx}"'
|
||||
|
||||
def __init__(self, *, test_ctx: int) -> None:
|
||||
super().__init__(test_ctx=test_ctx)
|
||||
|
||||
with pytest.raises(TestError) as exc_info:
|
||||
raise TestError(test_ctx='test_value')
|
||||
assert str(exc_info.value) == 'test message template "test_value"'
|
||||
@@ -1,253 +0,0 @@
|
||||
from typing import Dict, List, Union
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.exceptions import ValidationError, flatten_errors
|
||||
|
||||
|
||||
@pytest.mark.parametrize('result,expected', (
|
||||
(
|
||||
'display_errors',
|
||||
"""\
|
||||
a
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
b -> x
|
||||
field required (type=value_error.missing)
|
||||
b -> z
|
||||
field required (type=value_error.missing)
|
||||
c -> 0 -> x
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
d
|
||||
invalid literal for int() with base 10: 'string' (type=value_error)
|
||||
d
|
||||
badly formed hexadecimal UUID string (type=value_error)
|
||||
e -> __key__
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
f -> 0
|
||||
int() argument must be a string, a bytes-like object or a number, not 'NoneType' (type=type_error)
|
||||
f -> 0
|
||||
None is not an allow value (type=type_error)""",
|
||||
),
|
||||
(
|
||||
'flatten_errors',
|
||||
[
|
||||
{
|
||||
'loc': (
|
||||
'a',
|
||||
),
|
||||
'msg': 'invalid literal for int() with base 10: \'not_int\'',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'b',
|
||||
'x',
|
||||
),
|
||||
'msg': 'field required',
|
||||
'type': 'value_error.missing',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'b',
|
||||
'z',
|
||||
),
|
||||
'msg': 'field required',
|
||||
'type': 'value_error.missing',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'c',
|
||||
0,
|
||||
'x',
|
||||
),
|
||||
'msg': 'invalid literal for int() with base 10: \'not_int\'',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'd',
|
||||
),
|
||||
'msg': 'invalid literal for int() with base 10: \'string\'',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'd',
|
||||
),
|
||||
'msg': 'badly formed hexadecimal UUID string',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'e',
|
||||
'__key__',
|
||||
),
|
||||
'msg': 'invalid literal for int() with base 10: \'not_int\'',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'f',
|
||||
0,
|
||||
),
|
||||
'msg': 'int() argument must be a string, a bytes-like object or a number, not \'NoneType\'',
|
||||
'type': 'type_error',
|
||||
},
|
||||
{
|
||||
'loc': (
|
||||
'f',
|
||||
0,
|
||||
),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
},
|
||||
],
|
||||
),
|
||||
(
|
||||
'json',
|
||||
"""\
|
||||
[
|
||||
{
|
||||
"loc": [
|
||||
"a"
|
||||
],
|
||||
"msg": "invalid literal for int() with base 10: 'not_int'",
|
||||
"type": "value_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"b",
|
||||
"x"
|
||||
],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"b",
|
||||
"z"
|
||||
],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"c",
|
||||
0,
|
||||
"x"
|
||||
],
|
||||
"msg": "invalid literal for int() with base 10: 'not_int'",
|
||||
"type": "value_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"d"
|
||||
],
|
||||
"msg": "invalid literal for int() with base 10: 'string'",
|
||||
"type": "value_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"d"
|
||||
],
|
||||
"msg": "badly formed hexadecimal UUID string",
|
||||
"type": "value_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"e",
|
||||
"__key__"
|
||||
],
|
||||
"msg": "invalid literal for int() with base 10: 'not_int'",
|
||||
"type": "value_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"f",
|
||||
0
|
||||
],
|
||||
"msg": "int() argument must be a string, a bytes-like object or a number, not 'NoneType'",
|
||||
"type": "type_error"
|
||||
},
|
||||
{
|
||||
"loc": [
|
||||
"f",
|
||||
0
|
||||
],
|
||||
"msg": "None is not an allow value",
|
||||
"type": "type_error"
|
||||
}
|
||||
]"""
|
||||
),
|
||||
(
|
||||
'__str__',
|
||||
"""\
|
||||
validation errors
|
||||
a
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
b -> x
|
||||
field required (type=value_error.missing)
|
||||
b -> z
|
||||
field required (type=value_error.missing)
|
||||
c -> 0 -> x
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
d
|
||||
invalid literal for int() with base 10: 'string' (type=value_error)
|
||||
d
|
||||
badly formed hexadecimal UUID string (type=value_error)
|
||||
e -> __key__
|
||||
invalid literal for int() with base 10: 'not_int' (type=value_error)
|
||||
f -> 0
|
||||
int() argument must be a string, a bytes-like object or a number, not 'NoneType' (type=type_error)
|
||||
f -> 0
|
||||
None is not an allow value (type=type_error)"""
|
||||
),
|
||||
))
|
||||
def test_validation_error(result, expected):
|
||||
class SubModel(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
z: str
|
||||
|
||||
class Model(BaseModel):
|
||||
a: int
|
||||
b: SubModel
|
||||
c: List[SubModel]
|
||||
d: Union[int, UUID]
|
||||
e: Dict[int, str]
|
||||
f: List[Union[int, str]]
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model.parse_obj({
|
||||
'a': 'not_int',
|
||||
'b': {
|
||||
'y': 42,
|
||||
},
|
||||
'c': [
|
||||
{
|
||||
'x': 'not_int',
|
||||
'y': 42,
|
||||
'z': 'string',
|
||||
},
|
||||
],
|
||||
'd': 'string',
|
||||
'e': {
|
||||
'not_int': 'string',
|
||||
},
|
||||
'f': [
|
||||
None,
|
||||
],
|
||||
})
|
||||
|
||||
result = getattr(exc_info.value, result)
|
||||
if callable(result):
|
||||
result = result()
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_flatten_errors_unknown_error_object():
|
||||
with pytest.raises(RuntimeError):
|
||||
list(flatten_errors([object]))
|
||||
+20
-17
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel, ConfigError, NoneBytes, NoneStr, Required, ValidationError, constr
|
||||
from pydantic import BaseModel, NoneBytes, NoneStr, Required, ValidationError, constr, errors
|
||||
|
||||
|
||||
def test_success():
|
||||
@@ -40,13 +40,13 @@ def test_ultra_simple_failed():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'could not convert string to float: \'x\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid float',
|
||||
'type': 'type_error.float',
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'invalid literal for int() with base 10: \'x\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_ultra_simple_repr():
|
||||
m = UltraSimpleModel(a=10.2)
|
||||
assert repr(m) == '<UltraSimpleModel a=10.2 b=10>'
|
||||
assert repr(m.fields['a']) == ("<Field a: type='float', required=True, "
|
||||
"validators=['float', 'number_size_validator']>")
|
||||
"validators=['float_validator']>")
|
||||
assert dict(m) == {'a': 10.2, 'b': 10}
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ def test_nullable_strings_fails():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('required_str_value',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
{
|
||||
'loc': ('required_bytes_value',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -198,21 +198,21 @@ class InvalidValidator:
|
||||
|
||||
|
||||
def test_invalid_validator():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class InvalidValidatorModel(BaseModel):
|
||||
x: InvalidValidator = ...
|
||||
assert exc_info.value.args[0].startswith('Invalid signature for validator')
|
||||
|
||||
|
||||
def test_no_validator():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class NoValidatorModel(BaseModel):
|
||||
x: object = ...
|
||||
assert exc_info.value.args[0] == "no validator found for <class 'object'>"
|
||||
|
||||
|
||||
def test_unable_to_infer():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class InvalidDefinitionModel(BaseModel):
|
||||
x = None
|
||||
assert exc_info.value.args[0] == 'unable to infer type for attribute "x"'
|
||||
@@ -398,8 +398,8 @@ def test_validating_assignment_fail():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'invalid literal for int() with base 10: \'b\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -408,8 +408,11 @@ def test_validating_assignment_fail():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'length less than minimum allowed: 1',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value has at least 1 characters',
|
||||
'type': 'value_error.any_str.min_length',
|
||||
'ctx': {
|
||||
'limit_value': 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ def test_sub_env_missing():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('apple',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
+324
-212
@@ -4,6 +4,7 @@ from collections import OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from enum import Enum, IntEnum
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
@@ -38,8 +39,11 @@ def test_constrained_str_too_long():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'length greater than maximum allowed: 10',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value has at most 10 characters',
|
||||
'type': 'value_error.any_str.max_length',
|
||||
'ctx': {
|
||||
'limit_value': 10,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -76,13 +80,13 @@ def test_dsn_no_driver():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('db_driver',),
|
||||
'msg': 'None is not an allow value',
|
||||
'type': 'type_error',
|
||||
'msg': 'none is not an allow value',
|
||||
'type': 'type_error.none.not_allowed',
|
||||
},
|
||||
{
|
||||
'loc': ('dsn',),
|
||||
'msg': '"db_driver" field may not be missing or None',
|
||||
'type': 'value_error',
|
||||
'msg': '"driver" field may not be empty',
|
||||
'type': 'value_error.dsn.driver_is_empty',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -99,8 +103,8 @@ def test_module_import():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('module',),
|
||||
'msg': '"foobar" doesn\'t look like a module path',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value contains valid import path',
|
||||
'type': 'type_error.py_object',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -117,7 +121,6 @@ class CheckModel(BaseModel):
|
||||
class Config:
|
||||
anystr_strip_whitespace = True
|
||||
max_anystr_length = 10
|
||||
max_number_size = 100
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value,result', [
|
||||
@@ -148,6 +151,10 @@ class CheckModel(BaseModel):
|
||||
('bytes_check', b's', b's'),
|
||||
('bytes_check', b' s ', b's'),
|
||||
('bytes_check', 1, b'1'),
|
||||
('bytes_check', bytearray('xx', encoding='utf8'), b'xx'),
|
||||
('bytes_check', True, b'True'),
|
||||
('bytes_check', False, b'False'),
|
||||
('bytes_check', {}, ValidationError),
|
||||
('bytes_check', 'x' * 11, ValidationError),
|
||||
('bytes_check', b'x' * 11, ValidationError),
|
||||
|
||||
@@ -159,9 +166,6 @@ class CheckModel(BaseModel):
|
||||
('int_check', 12, 12),
|
||||
('int_check', '12', 12),
|
||||
('int_check', b'12', 12),
|
||||
('int_check', 123, ValidationError),
|
||||
('int_check', '123', ValidationError),
|
||||
('int_check', b'123', ValidationError),
|
||||
|
||||
('float_check', 1, 1.0),
|
||||
('float_check', 1.0, 1.0),
|
||||
@@ -169,9 +173,6 @@ class CheckModel(BaseModel):
|
||||
('float_check', '1', 1.0),
|
||||
('float_check', b'1.0', 1.0),
|
||||
('float_check', b'1', 1.0),
|
||||
('float_check', 123, ValidationError),
|
||||
('float_check', '123', ValidationError),
|
||||
('float_check', b'123', ValidationError),
|
||||
|
||||
('uuid_check', 'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
|
||||
('uuid_check', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
|
||||
@@ -210,8 +211,11 @@ def test_string_too_long():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('str_check',),
|
||||
'msg': 'length greater than maximum allowed: 10',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value has at most 10 characters',
|
||||
'type': 'value_error.any_str.max_length',
|
||||
'ctx': {
|
||||
'limit_value': 10,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -222,51 +226,11 @@ def test_string_too_short():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('str_check',),
|
||||
'msg': 'length less than minimum allowed: 5',
|
||||
'type': 'value_error',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class NumberModel(BaseModel):
|
||||
int_check: int
|
||||
float_check: float
|
||||
|
||||
class Config:
|
||||
min_number_size = 5
|
||||
max_number_size = 10
|
||||
|
||||
|
||||
def test_number_too_big():
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
NumberModel(int_check=50, float_check=150)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('int_check',),
|
||||
'msg': 'size greater than maximum allowed: 10',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('float_check',),
|
||||
'msg': 'size greater than maximum allowed: 10',
|
||||
'type': 'value_error',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_number_too_small():
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
NumberModel(int_check=1, float_check=2.5)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('int_check',),
|
||||
'msg': 'size less than minimum allowed: 5',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('float_check',),
|
||||
'msg': 'size less than minimum allowed: 5',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value has at least 5 characters',
|
||||
'type': 'value_error.any_str.min_length',
|
||||
'ctx': {
|
||||
'limit_value': 5,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -302,23 +266,23 @@ def test_datetime_errors():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('dt',),
|
||||
'msg': 'month must be in 1..12',
|
||||
'type': 'value_error',
|
||||
'msg': 'invalid datetime format',
|
||||
'type': 'type_error.datetime',
|
||||
},
|
||||
{
|
||||
'loc': ('date_',),
|
||||
'msg': 'Invalid date format',
|
||||
'type': 'value_error',
|
||||
'msg': 'invalid date format',
|
||||
'type': 'type_error.date',
|
||||
},
|
||||
{
|
||||
'loc': ('time_',),
|
||||
'msg': 'hour must be in 0..23',
|
||||
'type': 'value_error',
|
||||
'msg': 'invalid time format',
|
||||
'type': 'type_error.time',
|
||||
},
|
||||
{
|
||||
'loc': ('duration',),
|
||||
'msg': 'Invalid duration format',
|
||||
'type': 'value_error',
|
||||
'msg': 'invalid duration format',
|
||||
'type': 'type_error.duration',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -351,9 +315,9 @@ def test_enum_fails():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('tool',),
|
||||
'msg': '3 is not a valid ToolEnum',
|
||||
'type': 'value_error',
|
||||
}
|
||||
'msg': 'value is not a valid enumeration member',
|
||||
'type': 'type_error.enum',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -406,22 +370,28 @@ def test_string_fails():
|
||||
{
|
||||
'loc': ('str_regex',),
|
||||
'msg': 'string does not match regex "^xxx\\d{3}$"',
|
||||
'type': 'value_error',
|
||||
'type': 'value_error.str.regex',
|
||||
'ctx': {
|
||||
'pattern': '^xxx\\d{3}$',
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('str_min_length',),
|
||||
'msg': 'length less than minimum allowed: 5',
|
||||
'type': 'value_error',
|
||||
'msg': 'ensure this value has at least 5 characters',
|
||||
'type': 'value_error.any_str.min_length',
|
||||
'ctx': {
|
||||
'limit_value': 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('str_email',),
|
||||
'msg': 'The email address contains invalid characters before the @-sign: <.',
|
||||
'type': 'value_error.email_not_valid_error.email_syntax_error',
|
||||
'msg': 'value is not a valid email address',
|
||||
'type': 'value_error.email',
|
||||
},
|
||||
{
|
||||
'loc': ('name_email',),
|
||||
'msg': 'The email address contains invalid characters before the @-sign: .',
|
||||
'type': 'value_error.email_not_valid_error.email_syntax_error',
|
||||
'msg': 'value is not a valid email address',
|
||||
'type': 'value_error.email',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -440,151 +410,176 @@ def test_email_validator_not_installed_name_email():
|
||||
str_email: NameEmail = ...
|
||||
|
||||
|
||||
class ListDictTupleModel(BaseModel):
|
||||
a: dict = None
|
||||
b: list = None
|
||||
c: OrderedDict = None
|
||||
d: tuple = None
|
||||
|
||||
|
||||
def test_dict():
|
||||
assert ListDictTupleModel(a={1: 10, 2: 20}).a == {1: 10, 2: 20}
|
||||
assert ListDictTupleModel(a=[(1, 2), (3, 4)]).a == {1: 2, 3: 4}
|
||||
class Model(BaseModel):
|
||||
v: dict
|
||||
|
||||
assert Model(v={1: 10, 2: 20}).v == {1: 10, 2: 20}
|
||||
assert Model(v=[(1, 2), (3, 4)]).v == {1: 2, 3: 4}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ListDictTupleModel(a=[1, 2, 3])
|
||||
Model(v=[1, 2, 3])
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'value is not a valid dict, got list',
|
||||
'type': 'type_error',
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid dict',
|
||||
'type': 'type_error.dict',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_list():
|
||||
m = ListDictTupleModel(b=[1, 2, '3'])
|
||||
assert m.a is None
|
||||
assert m.b == [1, 2, '3']
|
||||
assert ListDictTupleModel(b='xyz').b == ['x', 'y', 'z']
|
||||
assert ListDictTupleModel(b=(i**2 for i in range(5))).b == [0, 1, 4, 9, 16]
|
||||
class Model(BaseModel):
|
||||
v: list
|
||||
|
||||
assert Model(v=[1, 2, '3']).v == [1, 2, '3']
|
||||
assert Model(v='xyz').v == ['x', 'y', 'z']
|
||||
assert Model(v=(i**2 for i in range(5))).v == [0, 1, 4, 9, 16]
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ListDictTupleModel(b=1)
|
||||
Model(v=1)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': '\'int\' object is not iterable',
|
||||
'type': 'type_error',
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid list',
|
||||
'type': 'type_error.list',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_ordered_dict():
|
||||
assert ListDictTupleModel(c=OrderedDict([(1, 10), (2, 20)])).c == OrderedDict([(1, 10), (2, 20)])
|
||||
assert ListDictTupleModel(c={1: 10, 2: 20}).c in (OrderedDict([(1, 10), (2, 20)]), OrderedDict([(2, 20), (1, 10)]))
|
||||
assert ListDictTupleModel(c=[(1, 2), (3, 4)]).c == OrderedDict([(1, 2), (3, 4)])
|
||||
class Model(BaseModel):
|
||||
v: OrderedDict
|
||||
|
||||
assert Model(v=OrderedDict([(1, 10), (2, 20)])).v == OrderedDict([(1, 10), (2, 20)])
|
||||
assert Model(v={1: 10, 2: 20}).v in (OrderedDict([(1, 10), (2, 20)]), OrderedDict([(2, 20), (1, 10)]))
|
||||
assert Model(v=[(1, 2), (3, 4)]).v == OrderedDict([(1, 2), (3, 4)])
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ListDictTupleModel(c=[1, 2, 3])
|
||||
Model(v=[1, 2, 3])
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': '\'int\' object is not iterable',
|
||||
'type': 'type_error',
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid dict',
|
||||
'type': 'type_error.dict',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_tuple():
|
||||
m = ListDictTupleModel(d=(1, 2, '3'))
|
||||
assert m.a is None
|
||||
assert m.d == (1, 2, '3')
|
||||
assert m.dict() == {'a': None, 'b': None, 'c': None, 'd': (1, 2, '3')}
|
||||
assert ListDictTupleModel(d='xyz').d == ('x', 'y', 'z')
|
||||
assert ListDictTupleModel(d=(i**2 for i in range(5))).d == (0, 1, 4, 9, 16)
|
||||
class Model(BaseModel):
|
||||
v: tuple
|
||||
|
||||
assert Model(v=(1, 2, '3')).v == (1, 2, '3')
|
||||
assert Model(v='xyz').v == ('x', 'y', 'z')
|
||||
assert Model(v=(i**2 for i in range(5))).v == (0, 1, 4, 9, 16)
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ListDictTupleModel(d=1)
|
||||
Model(v=1)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('d',),
|
||||
'msg': '\'int\' object is not iterable',
|
||||
'type': 'type_error',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class IntModel(BaseModel):
|
||||
a: PositiveInt = None
|
||||
b: NegativeInt = None
|
||||
c: conint(gt=4, lt=10) = None
|
||||
|
||||
|
||||
def test_int_validation():
|
||||
m = IntModel(a=5, b=-5, c=5)
|
||||
assert m == {'a': 5, 'b': -5, 'c': 5}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IntModel(a=-5, b=5, c=-5)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'size less than minimum allowed: 0',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'size greater than maximum allowed: 0',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': 'size less than minimum allowed: 4',
|
||||
'type': 'value_error',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class FloatModel(BaseModel):
|
||||
a: PositiveFloat = None
|
||||
b: NegativeFloat = None
|
||||
c: confloat(gt=4, lt=12.2) = None
|
||||
|
||||
|
||||
def test_float_validation():
|
||||
m = FloatModel(a=5.1, b=-5.2, c=5.3)
|
||||
assert m == {'a': 5.1, 'b': -5.2, 'c': 5.3}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
FloatModel(a=-5.1, b=5.2, c=-5.3)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'size less than minimum allowed: 0',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'size greater than maximum allowed: 0',
|
||||
'type': 'value_error',
|
||||
},
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': 'size less than minimum allowed: 4',
|
||||
'type': 'value_error',
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid tuple',
|
||||
'type': 'type_error.tuple',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_set():
|
||||
class SetModel(BaseModel):
|
||||
v: set = ...
|
||||
class Model(BaseModel):
|
||||
v: set
|
||||
|
||||
m = SetModel(v=[1, 2, 3])
|
||||
assert m.v == {1, 2, 3}
|
||||
assert m.dict() == {'v': {1, 2, 3}}
|
||||
assert SetModel(v={'a', 'b', 'c'}).v == {'a', 'b', 'c'}
|
||||
assert Model(v={1, 2, 2, '3'}).v == {1, 2, '3'}
|
||||
assert Model(v='xyzxyz').v == {'x', 'y', 'z'}
|
||||
assert Model(v={i**2 for i in range(5)}).v == {0, 1, 4, 9, 16}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(v=1)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'value is not a valid set',
|
||||
'type': 'type_error.set',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_int_validation():
|
||||
class Model(BaseModel):
|
||||
a: PositiveInt = None
|
||||
b: NegativeInt = None
|
||||
c: conint(gt=4, lt=10) = None
|
||||
|
||||
m = Model(a=5, b=-5, c=5)
|
||||
assert m == {'a': 5, 'b': -5, 'c': 5}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(a=-5, b=5, c=-5)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'ensure this value is greater than 0',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'ensure this value is less than 0',
|
||||
'type': 'value_error.number.max_size',
|
||||
'ctx': {
|
||||
'limit_value': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': 'ensure this value is greater than 4',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': 4,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_float_validation():
|
||||
class Model(BaseModel):
|
||||
a: PositiveFloat = None
|
||||
b: NegativeFloat = None
|
||||
c: confloat(gt=4, lt=12.2) = None
|
||||
|
||||
m = Model(a=5.1, b=-5.2, c=5.3)
|
||||
assert m == {'a': 5.1, 'b': -5.2, 'c': 5.3}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(a=-5.1, b=5.2, c=-5.3)
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'ensure this value is greater than 0',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'ensure this value is less than 0',
|
||||
'type': 'value_error.number.max_size',
|
||||
'ctx': {
|
||||
'limit_value': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': 'ensure this value is greater than 4',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': 4,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_strict_str():
|
||||
@@ -609,8 +604,8 @@ def test_uuid_error():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('v',),
|
||||
'msg': 'badly formed hexadecimal UUID string',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid uuid',
|
||||
'type': 'type_error.uuid',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -644,23 +639,35 @@ def test_uuid_validation():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'uuid version 1 expected, not 5',
|
||||
'type': 'value_error',
|
||||
'msg': 'uuid version 1 expected',
|
||||
'type': 'value_error.uuid.version',
|
||||
'ctx': {
|
||||
'required_version': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('b',),
|
||||
'msg': 'uuid version 3 expected, not 4',
|
||||
'type': 'value_error',
|
||||
'msg': 'uuid version 3 expected',
|
||||
'type': 'value_error.uuid.version',
|
||||
'ctx': {
|
||||
'required_version': 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('c',),
|
||||
'msg': 'uuid version 4 expected, not 3',
|
||||
'type': 'value_error',
|
||||
'msg': 'uuid version 4 expected',
|
||||
'type': 'value_error.uuid.version',
|
||||
'ctx': {
|
||||
'required_version': 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
'loc': ('d',),
|
||||
'msg': 'uuid version 5 expected, not 1',
|
||||
'type': 'value_error',
|
||||
'msg': 'uuid version 5 expected',
|
||||
'type': 'value_error.uuid.version',
|
||||
'ctx': {
|
||||
'required_version': 5,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -693,34 +700,139 @@ def test_anystr_strip_whitespace_disabled():
|
||||
|
||||
@pytest.mark.parametrize('type_,value,result', [
|
||||
(condecimal(gt=Decimal('42.24')), Decimal('43'), Decimal('43')),
|
||||
(condecimal(gt=Decimal('42.24')), Decimal('42'), ValidationError),
|
||||
(condecimal(gt=Decimal('42.24')), Decimal('42'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure this value is greater than 42.24',
|
||||
'type': 'value_error.number.min_size',
|
||||
'ctx': {
|
||||
'limit_value': Decimal('42.24'),
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(lt=Decimal('42.24')), Decimal('42'), Decimal('42')),
|
||||
(condecimal(lt=Decimal('42.24')), Decimal('43'), ValidationError),
|
||||
(condecimal(lt=Decimal('42.24')), Decimal('43'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure this value is less than 42.24',
|
||||
'type': 'value_error.number.max_size',
|
||||
'ctx': {
|
||||
'limit_value': Decimal('42.24'),
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')),
|
||||
(condecimal(max_digits=2, decimal_places=1), Decimal('0.99'), ValidationError),
|
||||
(condecimal(max_digits=3, decimal_places=1), Decimal('999'), ValidationError),
|
||||
(condecimal(max_digits=2, decimal_places=1), Decimal('0.99'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure that there are no more than 1 decimal places',
|
||||
'type': 'value_error.decimal.max_places',
|
||||
'ctx': {
|
||||
'decimal_places': 1,
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(max_digits=3, decimal_places=1), Decimal('999'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure that there are no more than 2 digits before the decimal point',
|
||||
'type': 'value_error.decimal.whole_digits',
|
||||
'ctx': {
|
||||
'whole_digits': 2,
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(max_digits=4, decimal_places=1), Decimal('999'), Decimal('999')),
|
||||
(condecimal(max_digits=20, decimal_places=2), Decimal('742403889818000000'), Decimal('742403889818000000')),
|
||||
(condecimal(max_digits=20, decimal_places=2), Decimal('7.42403889818E+17'), Decimal('7.42403889818E+17')),
|
||||
(condecimal(max_digits=20, decimal_places=2), Decimal('7424742403889818000000'), ValidationError),
|
||||
(condecimal(max_digits=20, decimal_places=2), Decimal('7424742403889818000000'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure that there are no more than 20 digits in total',
|
||||
'type': 'value_error.decimal.max_digits',
|
||||
'ctx': {
|
||||
'max_digits': 20,
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')),
|
||||
(condecimal(max_digits=5, decimal_places=2), Decimal('7304E-3'), ValidationError),
|
||||
(condecimal(max_digits=5, decimal_places=2), Decimal('7304E-3'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure that there are no more than 2 decimal places',
|
||||
'type': 'value_error.decimal.max_places',
|
||||
'ctx': {
|
||||
'decimal_places': 2,
|
||||
},
|
||||
},
|
||||
]),
|
||||
(condecimal(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')),
|
||||
(condecimal(max_digits=5, decimal_places=5), Decimal('70E-6'), ValidationError),
|
||||
(condecimal(max_digits=5, decimal_places=5), Decimal('70E-6'), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'ensure that there are no more than 5 digits in total',
|
||||
'type': 'value_error.decimal.max_digits',
|
||||
'ctx': {
|
||||
'max_digits': 5,
|
||||
},
|
||||
},
|
||||
]),
|
||||
*[
|
||||
(condecimal(decimal_places=2, max_digits=10), Decimal(value), ValidationError)
|
||||
(condecimal(decimal_places=2, max_digits=10), value, [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'value is not a valid decimal',
|
||||
'type': 'value_error.decimal.not_finite',
|
||||
},
|
||||
])
|
||||
for value in (
|
||||
'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN',
|
||||
'Inf', '-Inf', '+Inf', 'Infinity', '-Infinity', '-Infinity',
|
||||
'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN', 'Inf', '-Inf', '+Inf',
|
||||
'Infinity', '-Infinity', '-Infinity',
|
||||
)
|
||||
],
|
||||
*[
|
||||
(condecimal(decimal_places=2, max_digits=10), Decimal(value), [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'value is not a valid decimal',
|
||||
'type': 'value_error.decimal.not_finite',
|
||||
},
|
||||
])
|
||||
for value in (
|
||||
'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN', 'Inf', '-Inf', '+Inf',
|
||||
'Infinity', '-Infinity', '-Infinity',
|
||||
)
|
||||
],
|
||||
])
|
||||
def test_decimal_validation(type_, value, result):
|
||||
model = create_model('DecimalModel', foo=(type_, ...))
|
||||
kwargs = {'foo': value}
|
||||
|
||||
if result == ValidationError:
|
||||
with pytest.raises(ValidationError):
|
||||
model(**kwargs)
|
||||
if not isinstance(result, Decimal):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
model(foo=value)
|
||||
assert exc_info.value.flatten_errors() == result
|
||||
else:
|
||||
assert model(**kwargs).dict()['foo'] == result
|
||||
assert model(foo=value).foo == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value,result', (
|
||||
('/test/path', Path('/test/path')),
|
||||
(Path('/test/path'), Path('/test/path')),
|
||||
(None, [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'value is not a valid path',
|
||||
'type': 'type_error.path',
|
||||
},
|
||||
]),
|
||||
))
|
||||
def test_path_validation(value, result):
|
||||
class Model(BaseModel):
|
||||
foo: Path
|
||||
|
||||
if not isinstance(result, Path):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(foo=value)
|
||||
assert exc_info.value.flatten_errors() == result
|
||||
else:
|
||||
assert Model(foo=value).foo == result
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel, ConfigError, ValidationError, validator
|
||||
from pydantic import BaseModel, ValidationError, errors, validator
|
||||
|
||||
|
||||
def test_simple():
|
||||
@@ -140,8 +140,8 @@ def test_validating_assignment_dict():
|
||||
assert exc_info.value.flatten_errors() == [
|
||||
{
|
||||
'loc': ('a',),
|
||||
'msg': 'invalid literal for int() with base 10: \'x\'',
|
||||
'type': 'value_error',
|
||||
'msg': 'value is not a valid integer',
|
||||
'type': 'type_error.integer',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_classmethod():
|
||||
|
||||
|
||||
def test_duplicates():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
b: str
|
||||
@@ -208,7 +208,7 @@ def test_duplicates():
|
||||
|
||||
|
||||
def test_use_bare():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
|
||||
@@ -219,7 +219,7 @@ def test_use_bare():
|
||||
|
||||
|
||||
def test_use_no_fields():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
|
||||
@@ -320,7 +320,7 @@ def test_wildcard_validator_error():
|
||||
|
||||
|
||||
def test_invalid_field():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
|
||||
|
||||
Reference in New Issue
Block a user