-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Errors format #179
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
Errors format #179
Conversation
Codecov Report
@@ Coverage Diff @@
## master #179 +/- ##
==========================================
- Coverage 100% 99.89% -0.11%
==========================================
Files 9 9
Lines 988 992 +4
Branches 209 205 -4
==========================================
+ Hits 988 991 +3
- Partials 0 1 +1 |
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.
Looking very good, thank you very much.
I haven't been through everything in detail, but here are my initial thoughts.
To answer your questions:
- I don't mind whether we do it in one PR or multiple but I think it'll cause less pain for users if we release all the changes in one go, eg. before we next release we should move to
contextetc. Maybe a new PR. - Maybe we should use
__key__to reduce the chance of confusion with a real key? - I think we should have one error per location. The point here is to provide the most usful information and duplicate error will be a brainfuck for end uers.
- I think this looks good
pydantic/exceptions.py
Outdated
|
|
||
| @property | ||
| def display_errors(self): | ||
| return '\n'.join(' ' * i + msg for i, msg in _render_errors(self.errors_dict)) | ||
| def flatten_errors(self): |
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 this is a property it's name shouldn't be a verb, just flat_errors is good.
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.
agree
pydantic/exceptions.py
Outdated
|
|
||
|
|
||
| Error = namedtuple('Error', ['exc', 'track', 'index']) | ||
| 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.
for a smaller performance gain, can we use __slots__ here.
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 this class isn't required at all and we should just store a tuple, named tuple or dict?
pydantic/exceptions.py
Outdated
| class Error: | ||
| def __init__(self, exc: Exception, *, loc: Union[str, int] = None) -> None: | ||
| self.exc_info = exc | ||
| self.exc_type = type(exc) |
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 this isn't necessary as we can call type(self.exc_info) when it's needed?
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.
yep
pydantic/fields.py
Outdated
| for i, v_ in v_iter: | ||
| single_result, single_errors = self._validate_singleton(v_, values, i, cls) | ||
| single_result, single_errors = self._validate_singleton(v_, values, f'{loc}.{i}', cls) |
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 keep loc as a tuple until it's rendered?
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.
yep
|
Also we will need to do something if a key includes a dot. I'm not sure what the solution is, but I guess the first step is to store the We could:
not sure. ideas? |
Agree with you, better to ship all changes in one release. I want to introduce context in another PR, this PR is too big already and it can be hard to review if I add more changes.
Good idea, also I think in future we can add ability to change
I think we can introduce one error, something like: What do you think?
Let's go with this option, it's simple to implement and can be very useful for developers. |
|
@samuelcolvin ping :) |
|
I'm away, will look on Wednesday. |
|
One more example to discuss about sub fields: Looking at this example I have no idea what to do with errors. |
Regarding multiple errors in the same location. There are basically two cases: 1. all type errorsWe get a type error when trying each sub-type. Here we should definitely try to return one saying This might require some changes to the errors we raise. For example in your example with 2. value errors / not all type errors.I guess here we really have to return all the errors. It's ugly, but it's the only way of retaining all the information someone might need. I'm sorry to change my mind but I guess the best way of doing this is to return each error separately, so have duplicate locations in the list of errors. We can do something pretty when displaying errors so we have I guess we can add |
No problem, it's already implemented so I don't need to change anything.
I don't think that we need to merge error messages, let's keep it as simple as possible. @samuelcolvin please review code and let me know if I need something to improve. |
|
ok, will do, busy today so might be tomorrow. |
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.
Looking good, mostly small things.
I think the larger changes can all be left to other PRs.
pydantic/exceptions.py
Outdated
| return str(type_) | ||
|
|
||
| class Error: | ||
| __slots__ = ( |
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.
nitpick, can we keep this concise:
__slots__ = 'exc_info', '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.
fixed
pydantic/exceptions.py
Outdated
|
|
||
|
|
||
| class ValidationError(ValueError): | ||
| __slots__ = ( |
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.
same.
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.
fixed
pydantic/exceptions.py
Outdated
|
|
||
|
|
||
| def display_errors(errors): | ||
| display = [] |
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
display = [
' -> '.join(e['loc']) + f'\n {e["msg"]} (type={e["type"]})' for e in errors
]or something would be more elegant
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
| return '\n'.join(display) | ||
|
|
||
|
|
||
| def flatten_errors(errors, *, loc=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.
I think cleaner to make this a generator then call list(flatten_errors(...)) where it's called above.
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.
Can you please describe why you want to see this as generator?
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.
it will be much cleaner and more explicit with less code.
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}')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.
great! I forgot about yield from
pydantic/exceptions.py
Outdated
| elif isinstance(error, list): | ||
| flat.extend(flatten_errors(error)) | ||
| else: | ||
| raise TypeError(f'Unknown error object: {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.
Maybe we should raise another type exception here (eg.RuntimeError) to avoid confusion with type errors elsewhere in pydantic?
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.
agree, RuntimeError better in this case
pydantic/exceptions.py
Outdated
|
|
||
| @property | ||
| def display_errors(self): | ||
| return '\n'.join(' ' * i + msg for i, msg in _render_errors(self.errors_dict)) | ||
| def flat_errors(self): |
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 we should change this to be a method and internal. eg. _flat_errors()
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 so, for example in my case I would like to access .flat_errors in my project and convert this simple representation to something else, also I don't want to use .json() (or .serialize()) I need Python object at first.
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.
ok, can you just make it a method.
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.
done
pydantic/exceptions.py
Outdated
| 'type': error.type_, | ||
| }) | ||
| elif isinstance(error, list): | ||
| flat.extend(flatten_errors(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.
I think this should only happen in the case of errors from a Union here we should check if all errors are type_errors and if so group them into on type_error.union.
Again maybe this should wait for another PR.
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're right, but I think better place to implement logic like this in method _validate_singleton, where we're iterating thought sub fields. And yes, let's discuss this in another PR.
pydantic/fields.py
Outdated
|
|
||
| result, errors = {}, [] | ||
| for k, v_ in v_iter.items(): | ||
| key_result, key_errors = self.key_field.validate(k, values, 'key', cls) | ||
| v_loc = loc, '__key__' |
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.
is it possible for loc to already be a tuple here?
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.
it can be, found an issue, thanks!
tests/test_exceptions.py
Outdated
| None, | ||
| ], | ||
| }) | ||
| assert exc_info.value.display_errors == """a |
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.
easier to read if you do
assert exc_info.value.display_errors == """\
a
invalid literal for int() with base 10: 'not_int' (type=value_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.
fixed
tests/test_exceptions.py
Outdated
|
|
||
|
|
||
| def test_validation_error_display_errors(): | ||
| with pytest.raises(ValidationError) as exc_info: |
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 maybe of these tests can be rewritten as one parameterized test.
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.
fixed
|
@samuelcolvin review it again please |
|
awesome, thank you very much. We'll need to remember to update history before the next release. |
* changes to sequences to improve performance * add list[any] pydantic benchmark
Hi!
This is my first try to improve errors format (discussion can be found in #162). I'd like to get review on changes and discuss some points.
At first here are simple example which will be used to show new errors format:
And errors output:
[ { "loc": "a.0.x", "msg": "invalid literal for int() with base 10: 'not_int'", "type": "value_error" }, { "loc": "b", "msg": "invalid literal for int() with base 10: 'string'", "type": "value_error" }, { "loc": "c.x", "msg": "field required", "type": "value_error.missing" }, { "loc": "c.z", "msg": "field required", "type": "value_error.missing" }, { "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: 'foo'", "type": "value_error" }, { "loc": "f.0", "msg": "invalid literal for int() with base 10: 'string'", "type": "value_error" }, { "loc": "f.0", "msg": "badly formed hexadecimal UUID string", "type": "value_error" } ]Questions:
msgkept for backward compatibility and can be removed,contextcan be easily added)?keypostfix if error occurred in dictionary key, is it okay?d(andf.0) because they are complex and contains sub fields, I think we need to expose this information inloc, maybe by using postfix, something liked.sub.0andd.sub.1. Any ideas?TODO:
exceptions.pymoduleutils.pymodule