-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
Introduce error code for validation errors. #3716
Conversation
This patch is meant to fix encode#3111, regarding comments made to encode#3137 and encode#3169. The `ValidationError` will now contain a `code` attribute. Before this patch, `ValidationError.detail` only contained a `dict` with values equal to a `list` of string error messages or directly a `list` containing string error messages. Now, the string error messages are replaced with `ValidationError`. This means that, depending on the case, you will not only get a string back but a all object containing both the error message and the error code, respectively `ValidationError.detail` and `ValidationError.code`. It is important to note that the `code` attribute is not relevant when the `ValidationError` represents a combination of errors and hence is `None` in such cases. The main benefit of this change is that the error message and error code are now accessible the custom exception handler and can be used to format the error response. An custom exception handler example is available in the `TestValidationErrorWithCode` test.
4710e8f
to
8c29efe
Compare
ping @xordoquy maybe :-) |
@@ -31,7 +32,8 @@ def test_invalid_serializer(self): | |||
serializer = self.Serializer(data={'char': 'abc'}) | |||
assert not serializer.is_valid() | |||
assert serializer.validated_data == {} | |||
assert serializer.errors == {'integer': ['This field is required.']} | |||
assert serializer.errors['integer'].detail == ['This field is required.'] |
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.
Change the return type of serializer.errors
in this way simply isn't an option.
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 could change the definition of errors
to return the initial type.
Would that be enough ?
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 I'm not understanding something here?
We simply can't change the return type of errors
- it'll break stacks of existing projects.
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.
Well what I mean is that I could keep the new ValidationError
in _errors
and change the definition of errors
so that it will keep returning the same type as before and avoid backward incompatibilities.
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.
Possible thad be okay, yes. Hard to say 100% for sure without seeing the change.
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.
K i will give it a shot and come up with a new version of this.
Thx for taking the time.
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.
Ta. Sorry it's such a thorny one! 😄
Inline comment addresses the main issue here. The change footprint on this pull request still isn't minimal enough. If we start with simply introducing an optional |
We should keep `Serializer.errors`'s return type unchanged in order to maintain backward compatibility. The error codes will only be propagated to the `exception_handler` or accessible through the `Serializer._errors` private attribute.
@tomchristie: I think I managed to keep backward compatibility in place this time ;-) |
Would be nice to get some feedback on this before the start of next week as I will have some spare time to work on it if required. Sorry for being pushy again ^^ |
If the PR was made without changing any existing tests then that'd better demonstrate that the new codes don't break or change any existing API. Difficult to review when so much pre-existing tested behavior also changes. |
Yes, I realize now that I focussed only on I don't know where my mind was, really sorry about that. I just ran the tests against the unchanged tests version and I ended up with 16 failures which are all the same as the following:
Calling We talked before of having a "string-like" object that could hold the error code, it is likely that this is where that string-like object should live. I'll make another attempt that doesn't touch the tests at all this time and I'll get back to you ^^ |
Ta. We'll be closer to a properly reviewable state then. |
81c0e8f
to
9f77509
Compare
`ValidationErrorMessage` is a string-like object that holds a code attribute. The code attribute has been removed from ValidationError to be able to maintain better backward compatibility. What this means is that `ValidationError` can accept either a regular string or a `ValidationErrorMessage` for its `detail` attribute.
9f77509
to
42f4c55
Compare
@tomchristie, so I gave it another try, it looks much simpler now with a lot less changes for the same end results. Hopefully we can proceed with the actual review now ;-) Side note: I plan to rebase this to one simple commit once this gets reviewed. |
@@ -211,7 +221,9 @@ def __call__(self, attrs): | |||
queryset = self.exclude_current_instance(attrs, queryset) | |||
if queryset.exists(): | |||
message = self.message.format(date_field=self.date_field) | |||
raise ValidationError({self.field: message}) | |||
raise ValidationError({ | |||
self.field: ValidationErrorMessage(message, code='unique'), |
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.
Not 100% convinced by the new ValidationErrorMessage
and the change in the error argument for nested cases here. It ends up being a bit odd because in the nested case these's a different way of indicating the error code, than is used in the simple case.
Alternatives might include (1) constraining the code
argument to only support simple cases, or (2) support code
as a nested structure.
We might need to do some more thinking in this area. Unsure.
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 explain how the error argument for nested cases changes here ? I'm not following that part.
From what I get, everything stayed the same, it's just that the message
became a little bit smarter than before and it is opt-in so you'd only have to change anything if you actually want to use error codes.
`ValidationErrorMessage` is now abstracted in `ValidationError`'s constructor
dc6b4e8
to
7971343
Compare
See my hidden comment on the outdated diff above ;-) |
" @johnraz Okay noted. Tho let's still see how error codes are presented in Django's Forms API before making any further judgements. |
@tomchristie : this stackoverflow answer is quite useful http://stackoverflow.com/a/24874373/925854:
|
And the django documentation on the subject: |
So my current view on this is simple: We can divert from django's way by using
This solution doesn't have any backward incompatibility. OR We can stick to django's way by nesting
This solution will actually break backward compatibility for anyone who used to manipulate This is where this would happen:
|
How I would like to see this proceed:
|
So this is the state of what I had in johnraz@1834760 (and to be more specific johnraz@1834760#diff-80f43677c94659fbd60c8687cce68eafR253) So, you are okay with breaking the 16 tests that I pointed out earlier in the discussion ?
The problem with this approach is that you can have multiple errors for a single key in the error dict:
It will be a pain to mach the error with the error code Shouldn't we instead do something like:
Or maybe this is what you meant all along :) ? |
This is still a wip, the code is uggly and will need to be refactored and reviewed a lot!
8d2fb33
to
12e4b8f
Compare
else: | ||
self.detail = [self.full_details] | ||
|
||
self.detail = _force_text_recursive(self.detail) |
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.
There is obviously something wrong with all those isinstance checks and if you have any idea how to get rid of them or at least improve this code, I'm listening ;-)
As usually, I'd probably rather see just the Smallest coherent step possible at each point, please :) |
I can't help It, it seems :) Will simplify on tuesday \o/ |
😄 |
84094ad
to
44a1aab
Compare
There you go, I hope it is a bit simpler to read, nothing is done yet to deal with And yet the code is still very messy... |
44a1aab
to
2344d22
Compare
if code: | ||
self.full_details = ErrorDetails(detail, code) | ||
else: | ||
self.full_details = detail |
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.
We don't really want this stuff in. At least at this point.
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.
What do you mean ? Above or 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.
The population of .full_details
. Should just be storing the message and code at this point.
(Let me know if I'm being braindead tho - I only get a brief amount of time on each review)
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'm not sure to follow.
It seems to me that this is what I'm doing right now:
The population of .full_details. Should just be storing the message and code at this point.
It does store message and code for single items, a list when a list is provided and a dict when a dict is provided
Basically it does store what is passed to the __init__
method.
We then need to transform whatever is given as the detail
parameter to the former self.detail
format which is what this code does (quite painfully).
The more we progress on this, the more I'm convinced that the string-like object (aka If you have a minute to review those 2 commits in parallel now that you have a better understanding of what we try to achieve. It covers all the topics we discussed above (instantiation, exposing error message and code). It enables all the features (ability to handle error codes exposure in the All tests pass with a simpler code change and a nice API ( The only drawback is that it requires an explanation of how to get back the code in the johnraz@42f4c55 Sorry to insist but I think having a second thought here is healthy and I want to make sure we don't chose the wrong path and that we stay away from adding too much complexity. |
I created #3775 to help you compare ;-) |
@tomchristie: I'd also like to add to my argument in favor of the string-like object solution that it has the same impact on the api than the tuple solution has, in the end I cannot see any drawbacks by going with the string like object at all... |
Considering that the Also, if we wan't to make things more obvious, nothing stops us to implement |
Another argument: We don't need to reconstruct the (I think I'm done with arguments for today :-p) |
@tomchristie don't be afraid we will get to it :-) |
Bump ^^ :) |
Apologies - squeezed for time ATM. |
Accepted 👍 |
@tomchristie : any chance we could go back to this? I really think the proposal I made in #3775 is worth looking at and merging. @xordoquy maybe you could give it a shot too ? I just don't want to see all the efforts we put into this buried by time and loss of memory :) cheers ;-) |
We could try to, yup - want to address the failing travis build first? |
It fails on purpose - this is not yet complete. |
Closing this in favor of #3775 to reduce noise. |
This patch is meant to fix #3111, regarding comments made to #3137
and #3169.
The
ValidationError
will now contain acode
attribute.Before this patch,
ValidationError.detail
only contained adict
with values equal to alist
of string error messages ordirectly a
list
containing string error messages.Now, the string error messages are replaced with
ValidationError
.This means that, depending on the case, you will not only get a
string back but a full object containing both the error message and
the error code, respectively
ValidationError.detail
andValidationError.code
.It is important to note that the
code
attribute is not relevantwhen the
ValidationError
represents a combination of errors andhence is
None
in such cases.The main benefit of this change is that the error message and
error code are now accessible in the custom exception handler and can
be used to format the error response.
A custom exception handler example is available in the
TestValidationErrorWithCode
test.Regarding the other discussion:
build_error
method was droppedbuild_error_from_django_validation_error
has been kept because this is the only way to "convert" a regularDjangoValidationError
to a djrfValidationError
.@tomchristie @jpadilla @qsorix: if you don't mind giving this a look ;-)