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:
Nikita Grishko
2018-05-31 16:35:38 +03:00
committed by Samuel Colvin
parent 0698384788
commit 4f4e22ef47
21 changed files with 1307 additions and 733 deletions
+3
View File
@@ -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)
...................
+2 -1
View File
@@ -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
+27 -7
View File
@@ -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)
+140
View File
@@ -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', '')
+218
View File
@@ -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)
-97
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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',
},
]
+3 -3
View File
@@ -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)
+18 -17
View File
@@ -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
+324
View File
@@ -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_)
+16
View File
@@ -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"'
-253
View File
@@ -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
View File
@@ -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,
},
},
]
+2 -2
View File
@@ -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
View File
@@ -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
+7 -7
View File
@@ -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