10000 Error context and message by Gr1N · Pull Request #183 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c1110f0
POC of error context and message
Gr1N May 26, 2018
8a25c51
Move type errors to the `errors.py` module; Change errors interface a…
Gr1N May 27, 2018
b14c7bd
Merge branch 'master' into errors-context
Gr1N May 27, 2018
f8e4478
Rename `.as_dict()` to `.dict()`
Gr1N May 27, 2018
9692994
Fix `PydanticErrorMixin` constructor
Gr1N May 27, 2018
4ab5e69
Rename `exceptions.py` to `error_wrappers.py`
Gr1N May 28, 2018
15cd4e1
Do not include nullable `ctx`
Gr1N May 28, 2018
ed6d84e
Fix tests
Gr1N May 28, 2018
9e2ed7f
Added `int_validator`; Added `IntegerError`
Gr1N May 28, 2018
6447783
Added `float_validator`; Added `FloatError`
Gr1N May 28, 2018
01a20e5
Get rid of `__mro__` in prior of `exc.code`
Gr1N May 28, 2018
ecb2ecc
Removed `min_number_size` and `max_number_size` from config (#174)
Gr1N May 28, 2018
a2cc0a9
Added `NumberMinSizeError` and `NumberMaxSizeError`
Gr1N May 28, 2018
ef07e61
Added `NoneIsNotAllowedError`
Gr1N May 28, 2018
04d72e9
Added `EnumError`
Gr1N May 29, 2018
bc3bed8
Added `path_validator`; Added `PathError`
Gr1N May 29, 2018
a677a7e
Added `DictError`
Gr1N May 30, 2018
89804fb
Added `ListError`
Gr1N May 30, 2018
9e82397
Added `TupleError`
Gr1N May 30, 2018
6c2da85
Added `SetError`
Gr1N May 30, 2018
45f49c1
Added `datetime` related errors
Gr1N May 30, 2018
95b710f
Added `bytes` and `str` related errors
Gr1N May 30, 2018
af3d24f
Added `SequenceError`
Gr1N May 30, 2018
c5c906e
Improved code coverage
Gr1N May 30, 2018
6e62458
Display error context in string representation of validation error
Gr1N May 30, 2018
9b726ee
Redefine error message templates using config
Gr1N May 30, 2018
446cc76
Review fixes
Gr1N May 31, 2018
81df1c2
Updated changelog
Gr1N May 31, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
8000
Move type errors to the errors.py module; Change errors interface a…
… bit
  • Loading branch information
Gr1N committed May 27, 2018
commit 8a25c51e92c68c8f82371acd4b534b5e3e99892f
91 changes: 91 additions & 0 deletions pydantic/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
__all__ = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will lead to problems when people (eg. me) add errors and forget to update __all__, let's remove __all__.

If we're really worried about exposing Decimal etc. we could even import them as from decimal import Decimal as _Decimal, but I don't think it's necessary.

'PydanticErrorMixin',
'PydanticTypeError',
'PydanticValueError',

'ConfigError',

'MissingError',
'ExtraError',

'DecimalError',
'DecimalIsNotFiniteError',
'DecimalMaxDigitsError',
'DecimalMaxPlacesError',
'DecimalWholeDigitsError',

'UUIDError',
'UUIDVersionError',
)


class PydanticErrorMixin:
msg_template: str

def __init__(self, **ctx):
self.ctx = ctx or None

def __str__(self) -> str:
return self.msg_template.format(**self.ctx or {})


class PydanticTypeError(PydanticErrorMixin,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one line, we have 140 line limit.

TypeError):
pass


class PydanticValueError(PydanticErrorMixin,
ValueError):
pass


class ConfigError(RuntimeError):
pass


class MissingError(PydanticValueError):
msg_template = 'field required'


class ExtraError(PydanticValueError):
msg_template = 'extra fields not permitted'


class DecimalError(PydanticTypeError):
msg_template = 'value is not a valid decimal'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from gitter you said

In your example DecimalError inherited from TypeError, so if I inherit all decimal errors from DecimalError they will be type errors not value errors as now. Is it okay?

You're right, that's not ok, so the other decimal related exceptions below can't inherit from this DecimalError.

However we want the end type on the exception to be value_error.decimal.is_not_finite rather than value_error.decimal_is_not_finite. etc.

The way to do this is to add display_name (or similar) to exceptions:

class DecimalIsNotFiniteError(PydanticValueError):
    display_name = 'decimal.not_finite_error'

Then in exceptions._get_exc_bases do

name = getattr(b, 'display_name', None) or b.__name__
yield name.replace('Error', '')

does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to introduce display_name then I think we can simplify get_exc_type func logic to something like this:

def get_exc_type(exc: Exception) -> str:
    exc = type(exc)

    if issubclass(exc, TypeError):
        type_ = 'type_error'
    elif issubclass(exc, ValueError):
        type_ = 'value_error'
    else:
        RuntimeError(f'Unsupported exception: {exc}')

    display_name = getattr(exc, 'display_name', None)
    if display_name is not None:
        type_ = f'{type_}.{display_name}'

    return type_

And we can remove to_snake_case and all logic with MRO. Also I think it's more easy to understand and support. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree except that f'{type_}.{display_name}' I think will be wrong because to get decimal.not_finite_error and decimal.max_digits_exceeded you would need duplicate exception names

But maybe best for you to implement it and see how you go.



class DecimalIsNotFiniteError(PydanticValueError):
msg_template = 'value is not a valid decimal'


class DecimalMaxDigitsError(PydanticValueError):
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):
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):
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 UUIDError(PydanticTypeError):
msg_template = 'value is not a valid uuid'


class UUIDVersionError(PydanticValueError):
msg_template = 'uuid version {required_version} expected'

def __init__(self, *, required_version: int) -> None:
super().__init__(required_version=required_version)
119 changes: 7 additions & 112 deletions pydantic/exceptions.py
class ConfigError(RuntimeError):
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import json
from functools import lru_cache
from typing import Iterable, Type

from .errors import PydanticErrorMixin, PydanticTypeError, PydanticValueError
from .utils import to_snake_case

__all__ = (
'Error',
'ValidationError',
'ConfigError',

'ValueError_',
'TypeError_',

'MissingError',
'ExtraError',

'DecimalError',
'DecimalIsNotFiniteError',
'DecimalMaxDigitsError',
'DecimalMaxPlacesError',
'DecimalWholeDigitsError',

'UUIDError',
'UUIDVersionError',
)


Expand Down Expand Up @@ -78,89 +64,6 @@ def json(self, *, indent=2):
return json.dumps(self.flatten_errors(), indent=indent, sort_keys=True)


pass


class ValueError_(ValueError):
def __init__(self, msg_tmpl, **ctx):
self.ctx = ctx or None
self.msg_tmpl = msg_tmpl

super().__init__()

def __str__(self) -> str:
return self.msg_tmpl.format(**self.ctx or {})


class TypeError_(TypeError):
def __init__(self, msg_tmpl, **ctx):
self.ctx = ctx or None
self.msg_tmpl = msg_tmpl

super().__init__()

def __str__(self) -> str:
return self.msg_tmpl.format(**self.ctx or {})


class MissingError(ValueError_):
def __init__(self) -> None:
super().__init__('field required')


class ExtraError(ValueError_):
def __init__(self) -> None:
super().__init__('extra fields not permitted')


class DecimalError(TypeError_):
def __init__(self) -> None:
super().__init__('value is not a valid decimal')


class DecimalIsNotFiniteError(ValueError_):
def __init__(self) -> None:
super().__init__('value is not a valid decimal')


class DecimalMaxDigitsError(ValueError_):
def __init__(self, *, max_digits: int) -> None:
super().__init__(
'ensure that there are no more than {max_digits} digits in total',
max_digits=max_digits
)


class DecimalMaxPlacesError(ValueError_):
def __init__(self, *, decimal_places: int) -> None:
super().__init__(
'ensure that there are no more than {decimal_places} decimal places',
decimal_places=decimal_places
)


class DecimalWholeDigitsError(ValueError_):
def __init__(self, *, whole_digits: int) -> None:
super().__init__(
'ensure that there are no more than {whole_digits} digits before the decimal point',
whole_digits=whole_digits
)


class UUIDError(TypeError_):
def __init__(self):
super().__init__('value is not a valid uuid')


class UUIDVersionError(ValueError_):
def __init__(self, *, required_version: int) -> None:
super().__init__(
'uuid version {required_version} expected',
required_version=required_version
)


def display_errors(errors):
return '\n'.join(
f'{_display_error_loc(e["loc"])}\n {e["msg"]} (type={e["type"]})'
Expand All @@ -185,28 +88,20 @@ def flatten_errors(errors, *, loc=None):
raise RuntimeError(f'Unknown error object: {error}')


_EXC_TYPES = {}


@lru_cache()
def get_exc_type(exc: Exception) -> str:
exc = type(exc)
if exc in _EXC_TYPES:
return _EXC_TYPES[exc]

bases = tuple(_get_exc_bases(exc))
bases = tuple(_get_exc_bases(type(exc)))
bases = bases[::-1]

type_ = to_snake_case('.'.join(bases))
_EXC_TYPES[exc] = type_
return type_
return to_snake_case('.'.join(bases))


def _get_exc_bases(exc: Type[Exception]) -> Iterable[str]:
for b in exc.__mro__:
if b in (ValueError_, TypeError_,):
if b in (PydanticErrorMixin, PydanticTypeError, PydanticValueError):
continue

if b in (ValueError, TypeError):
if b in (TypeError, ValueError):
yield b.__name__
break

Expand Down
3 changes: 2 additions & 1 deletion pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from enum import IntEnum
from typing import Any, Callable, List, Mapping, NamedTuple, Set, Type, Union

from .exceptions import ConfigError, Error
from .errors import ConfigError
from .exceptions import Error
from .utils import display_as_type
from .validators import NoneType, dict_validator, find_validators, not_none_validator

Expand Down
3 changes: 2 additions & 1 deletion pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from types import FunctionType
from typing import Any, Dict, Set, Type, Union

from .exceptions import ConfigError, Error, ExtraError, MissingError, ValidationError
from .errors import ConfigError, ExtraError, MissingError
from .exceptions import Error, ValidationError
from .fields import Field, Validator
from .parse import Protocol, load_file, load_str_bytes
from .types import StrBytes
Expand Down
10 changes: 5 additions & 5 deletions pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Optional, Pattern, Type, Union
from uuid import UUID

from .exceptions import DecimalIsNotFiniteError, DecimalMaxDigitsError, DecimalMaxPlacesError, DecimalWholeDigitsError
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)
Expand Down Expand Up @@ -239,7 +239,7 @@ def get_validators(cls):
def validate(cls, value: Decimal) -> Decimal:
digit_tuple, exponent = value.as_tuple()[1:]
if exponent in {'F', 'n', 'N'}:
raise DecimalIsNotFiniteError()
raise errors.DecimalIsNotFiniteError()

if exponent >= 0:
# A positive exponent adds that many trailing zeros.
Expand All @@ -259,15 +259,15 @@ def validate(cls, value: Decimal) -> Decimal:
whole_digits = digits - decimals

if cls.max_digits is not None and digits > cls.max_digits:
raise DecimalMaxDigitsError(max_digits=cls.max_digits)
raise errors.DecimalMaxDigitsError(max_digits=cls.max_digits)

if cls.decimal_places is not None and decimals > cls.decimal_places:
raise DecimalMaxPlacesError(decimal_places=cls.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 DecimalWholeDigitsError(whole_digits=expected)
raise errors.DecimalWholeDigitsError(whole_digits=expected)

return value

Expand Down
14 changes: 7 additions & 7 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
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, DecimalError, DecimalIsNotFiniteError, UUIDError, UUIDVersionError
from .utils import display_as_type

NoneType = type(None)
Expand Down Expand Up @@ -135,14 +135,14 @@ def uuid_validator(v, field, config, **kwargs) -> UUID:
elif isinstance(v, (bytes, bytearray)):
v = UUID(v.decode())
except ValueError as e:
raise UUIDError() from e
raise errors.UUIDError() from e

if not isinstance(v, UUID):
raise UUIDError()
raise errors.UUIDError()

required_version = getattr(field.type_, '_required_version', None)
if required_version and v.version != required_version:
raise UUIDVersionError(required_version=required_version)
raise errors.UUIDVersionError(required_version=required_version)

return v

Expand All @@ -158,10 +158,10 @@ def decimal_validator(v) -> Decimal:
try:
v = Decimal(v)
except DecimalException as e:
raise DecimalError() from e
raise errors.DecimalError() from e

if not v.is_finite():
raise DecimalIsNotFiniteError()
raise errors.DecimalIsNotFiniteError()

return v

Expand Down Expand Up @@ -204,4 +204,4 @@ def find_validators(type_):
except TypeError as e:
# TODO: RuntimeError?
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 errors.ConfigError(f'no validator found for {type_}')
0