-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Error context and message #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I think this looks like a pretty good start.
Let's have a separate module for these errors (sorry to change my mind) then we can simply do
from pydantic import errors
...
raise errors.DecimalError()Combining a few of my comments, here is my suggestion for the rough layout of errors.py:
class _PydanticMixin:
default_msg_template = None
# __slots__ ?
def __init__(self, msg_template, **ctx):
self.ctx = ctx or None
self.msg_template = msg_template or self.default_msg_template
self.loc = None
super().__init__()
@property
def msg(self):
return str(self)
@property
def type_(self):
return get_exc_type(self)
def as_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
def __str__(self) -> str:
return self.msg_template.format(**self.ctx or {})
class PydanticValueError(_PydanticMixin, ValueError):
pass
class PydanticTypeError(_PydanticMixin, TypeError):
pass
class MissingError(PydanticValueError):
default_msg_template = 'field required'
class ExtraError(PydanticValueError):
default_msg_template = 'extra fields not permitted'
class DecimalError(PydanticTypeError):
default_msg_template = 'value is not a valid decimal'
class DecimalIsNotFiniteError(DecimalError):
default_msg_template = 'value is not a valid decimal'
...error_tmpls (let's call them error_templates for clarity) is a good idea, but perhaps we should set them when serialising the ValidationError rather than in config. In my experience I often have one place where I serialised ValidationError from lots of different models, eg. here I guess best to allow both.
pydantic/exceptions.py
Outdated
|
|
||
| class Missing(ValueError): | ||
| pass | ||
| class ValueError_(ValueError): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get your idea here, but let's call this PydanticValueError as it's more descriptive, same for PydanticTypeError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed
pydantic/exceptions.py
Outdated
| pass | ||
| class ValueError_(ValueError): | ||
| def __init__(self, msg_tmpl, **ctx): | ||
| self.ctx = ctx or None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could have
class _PydanticMixin:
...
class PydanticValueError(_PydanticMixin, ValueError):
pass
class PydanticTypeError(_PydanticMixin, TypeError):
passOr does that mess up mro below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
pydantic/exceptions.py
Outdated
| class ValueError_(ValueError): | ||
| def __init__(self, msg_tmpl, **ctx): | ||
| self.ctx = ctx or None | ||
| self.msg_tmpl = msg_tmpl |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe good to have
class ...
default_msg_tmpl = None
def __init__...:
self.msg_tmpl = msg_tmpl or self.default_msg_tmplThen many of the errors below can be simplified to
class ExtraError(ValueError_):
default_msg_tmpl = 'extra fields not permittedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also can we use template not tmpl, it could be confusing and it's not that much longer.
pydantic/exceptions.py
Outdated
| super().__init__('value is not a valid decimal') | ||
|
|
||
|
|
||
| class DecimalIsNotFiniteError(ValueError_): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess all these other decimal errors should inherit from DecimalError?
pydantic/exceptions.py
Outdated
| raise RuntimeError(f'Unknown error object: {error}') | ||
|
|
||
|
|
||
| _EXC_TYPES = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be more correct to use the lru_cache decorator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right
pydantic/exceptions.py
Outdated
| 'loc': loc, | ||
| 'msg': self.msg, | ||
| 'type': self.type_, | ||
| 'ctx': self.ctx, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not include ctx if it's None.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? For me it's better to return all keys with useful defaults, like None if there are no context. In this case user see that ctx is possible, but is empty now. Also from my point of view, better to write code like ctx = error.dict()['ctx'] rather than ctx = error.dict().get('ctx').
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to keep errors respones as short as possible. I understand your point of you but I'd still rather removing ctx if it's None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, fixed.
pydantic/exceptions.py
Outdated
|
|
||
| class Error: | ||
| __slots__ = 'exc_info', 'loc' | ||
| __slots__ = 'exc', 'loc' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can get rid of Error altogether and store loc on the Exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that we need to get rid of Error also I get your idea with _PydanticMixin which contains all about error (ctx, msg, loc, ...), but I don't think that this is a good idea.
To my mind this is not a good idea because:
ErrorandValidationErrorare wrappers around pydantic's validation logic, they hides inside errors representation and things likeloc;- moving logic into
_PydanticMixinwill broke your idea withTypeErrorandValueErrorwhich can be used by users to raise errors in user's validators; - in case if we want to store
locinside error as you proposed we need to rewrite a huge part because for now exceptions know nothing about where they raised.
I think we need to keep Error and ValidationError, but we need to change some things, maybe just naming. Also I want to keep Error because it's a good wrapper/container to hide validation logic from users and it's also helps us to change anything inside without introducing breaking changes.
I propose:
- move all errors like
MissingError,ExtraError,DecimalErrortoerrors.pymodule; - rename
ErrortoFieldError, because it's a error container which knows all about field error (exception and location) and it's a good place to work with config to get custom message templates; - rename
exceptions.pyto something more descriptive? likeerror_wrappers.py?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moving logic into
_PydanticMixinwill break your idea withTypeErrorandValueErrorwhich can be used by users to raise errors in user's validators
Very good point, let's Keep Error
- move all errors like
MissingError,ExtraError,DecimalErrortoerrors.pymodule;
Okay
- rename
ErrortoFieldError, because it's a error container which knows all about field error (exception and location) and it's a good place to work with config to get custom message templates;
I think Error is fine.
- rename
exceptions.pyto something more descriptive? likeerror_wrappers.py?
Okay
|
we should use |
|
|
||
|
|
||
| class DecimalError(PydanticTypeError): | ||
| msg_template = 'value is not a valid decimal' |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
@samuelcolvin please review |
Codecov Report
@@ Coverage Diff @@
## master #183 +/- ##
==========================================
- Coverage 100% 99.44% -0.56%
==========================================
Files 9 9
Lines 995 1088 +93
Branches 206 217 +11
==========================================
+ Hits 995 1082 +87
- Misses 0 4 +4
- Partials 0 2 +2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks awesome, I'm really excited to get it merged and deployed.
Just a few small things, also could you update HISTORY.rst, then I'll merge.
pydantic/errors.py
Outdated
| from decimal import Decimal | ||
| from typing import Union | ||
|
|
||
| __all__ = ( |
There was a problem hiding this comment.
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.
pydantic/errors.py
Outdated
| return self.msg_template.format(**self.ctx or {}) | ||
|
|
||
|
|
||
| class PydanticTypeError(PydanticErrorMixin, |
There was a problem hiding this comment.
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.
pydantic/error_wrappers.py
Outdated
| ) | ||
|
|
||
|
|
||
| class Error: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps we should rename toErrorWrapper for clarity?
pydantic/error_wrappers.py
Outdated
|
|
||
| code = getattr(exc, 'code', None) | ||
| if code is not None: | ||
| type_ = f'{type_}.{code}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we not still use to_snake_case (perhaps with re.sub('Error$', '', s) added) where code is None?
That would save quite a few lines from errors.py and allow pydantic to better support arbitrary errors.
|
@samuelcolvin check again please (: And please don't deploy new version, because I need to send one more PR with huge update for documentation, I would like to cover all about errors. Also I think we need to fix two moments: |
|
fixed #162. We need to remember to reference issues in PR comments! |
* working on PydanticValueError, fix pydantic#183 * rust class working * linting * add benchmarks * improvements to PydanticValueError * custom int context value processing * fix tests
This is an implementation of my vision of how to pass error context and build error message.
Also I'm thinking about redefining error template on config level, something like:
It's easy to implement we just need to pass config into
exceptions.Errorand improvemsgproperty logic.@samuelcolvin what do you think about PR and idea with config?