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
Add support for generics with __get_validators__ #1159
Add support for generics with __get_validators__ #1159
Conversation
This is awesome! Substantially easier than I even suspected haha. Let me know if you want help with docs. |
LGTM |
Thanks @dmontagu ! I was actually going to ask if we want this to be properly documented or is it enough of a corner case that we prefer to just support it without many docs. I'm happy to add docs if you think it's reasonable. |
Except to be consistent we should also allow the case where a generic doesn't implement |
tests/test_edge_cases.py
Outdated
@classmethod | ||
def validate(cls, v): | ||
if not isinstance(v, cls): | ||
raise ValueError('Invalid value') |
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.
Do isinstance(v.t1, cls.__args__[0])
and isinstance(v.t2, cls.__args__[1])
work here? I think it might be good to have an example that actually checks the generic types in the validators, for reference by others.
That could just go in the docs, but putting it in the tests too will help make sure it actually works 😅.
(In general you'd maybe want to handle some edge cases better than an isinstance check could, like the unparameterized version or with bounded TypeVars, but for a test/demonstration I think this would be fine.)
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.
Good point, I'm not sure it actually works, because the Field.type_
that is originally the generic is a type alias and the origin
is the one that has the __get_validators__
, but I'll try to see if it works.
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.
Hmm, I think it would be unfortunate if the implementation prevented you from actually checking the specific parameter types, but maybe that can be fixed later if necessary.
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.
Yeah, just checked.
Sadly, for validate
and __get_validators__
, cls
is a <class '__main__.MyGen'>
, not a __main__.MyGen[str, bool]
.
The way we could implement it would be to send the sub types to the validators provided by __get_validators__
as extra arguments, during validation.
But that would break the current API. I guess it would require another method.
Or, another option is to inspect each validator function returned to see if it requests the types. But that adds extra complexity and I'm not sure how much should we add for this version at least. That option would probably have some performance penalty too, so it might be better just as another 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.
It should be possible using the field
argument which can optionally be passed to a validator?
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.
Yeah, for performance reasons I think probably it should be the __get_validators__
function that receives the field type, and then that function yields "configured" validators.
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.
field
is already available as an argument to a valdator, it will only be provided if the validator takes that argument.
Are you sure it's not possible now?
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 hadn't seen these comments before writing my last comment down below. 😅
I'm realizing that
__get_validators__
is called on creation, so having some small extra logic for it shouldn't be too bad, as it's called only once on creation, duringpopulate_validators()
.Given that, I think I can make it accept an optional
FieldModel
, very much like in FastAPI (almost everywhere).The API would look like this:
if
__get_validators__
has a signature like:@classmethod def __get_validators__(cls, field: FieldModel): ...Then it would receive the
FieldModel
in thefield
argument.Otherwise, it would work as normal (no arguments).
Does that sound okay?
Edit: I think I was talking nonsense and there's nothing extra to implement. Validators seem to always be checked for a field
argument, I think it should work.
I'll try and add another shape for generics to distinguish them from tuples and refactor that part. But I think most of what's necessary is already there.
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.
Yeah, given that this is already implemented for validators that seems like the way to go! Awesome!
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.
raise ValueError('Invalid value') | |
raise TypeError('Invalid value') |
I'm neutral, I agree it's a corner case you would expect to work from the current docs, but might be worth adding a note to the custom types section just mentioning this? |
Codecov Report
@@ Coverage Diff @@
## master #1159 +/- ##
======================================
Coverage 100% 100%
======================================
Files 20 20
Lines 3479 3490 +11
Branches 674 679 +5
======================================
+ Hits 3479 3490 +11
Continue to review full report at Codecov.
|
So, if I'm happy with supporting |
I think so, but I'm wondering which cases we are ruling out with this line: https://github.com/samuelcolvin/pydantic/blob/1c49a3f689a959bec76f0b879a20510883938579/pydantic/fields.py#L422 I can't easily find the test that covers this -- any ideas how to find it? 😅 |
I was going to say "yes definitely", but I realise it's more complicated than that, see below.
put a fat |
duh 🤦♂️ I would personally vote that we allow such types (again, only assuming |
Humm, the problem is people assuming pydantic is doing validation when it's actually not doing much. We could:
No option there seems great to me, not sure what to do. |
Yeah, I kind of agree with everything... not sure what would be the best option here. On the other side, these type vars aren't only for class creation, for example: InT = TypeVar('InT')
OutT = TypeVar('OutT')
class MyGen2(Generic[InT, OutT]):
def __init__(self, input: InT):
self.input = input
def output() -> OutT:
# Something that returns OutT
return
g = MyGen2[str, bool](input="asdf")
g.output() # mypy infers this is a bool Here the |
And also, another option could be something like an alternative That way the validator function could take care of checking the Although this option would mean another custom dunder and a bigger refactor, because the generic type args are in the original type, but the |
Hmm, good investigation @tiangolo. I'll see if I can think of any clean way of resolving all of this, but given it probably won't be super straightforward, I'm personally happy with whatever will get the functionality working for you now. Presumably if we can find a way to smuggle in the field information into |
Thanks @dmontagu ! 🚀 Also, if this is all restricted to the user setting Another mitigation I can do is make an explicit example in the docs that doesn't use the type arguments from the generic on instantiation, that should help to avoid giving the idea that those types are checked. I'll wait before adding @samuelcolvin what do you think? It's your call 👑 |
I think this is fine, just the question above about whether it's already possible to validate the generic class's type. |
I think it's working quite well now. Including optional/custom validation for generic type parameters 🎉 I just had to add the field arg to the validator for it to get the So, given that, I refactored it all to have a new shape for generics, include sub-types, perform validation for the whole generic (not per-sub-type), etc. I'm using |
Check the new docs here: I added a small explicit section about And one for Generic types with validation: https://deploy-preview-1159--pydantic-docs.netlify.com/usage/types/#generic-classes-as-types |
tests/test_edge_cases.py
Outdated
@classmethod | ||
def validate(cls, v): | ||
if not isinstance(v, cls): | ||
raise ValueError('Invalid value') |
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.
raise ValueError('Invalid value') | |
raise TypeError('Invalid value') |
Thanks for the thorough code review @samuelcolvin , I just finished all the changes. |
* ✨ Add support for generics with __get_validators__ * ✅ Add tests for Generics with __get_validators__ * 📝 Add change note * ✨ Add support for Generic fields with validation of sub-types * 📝 Add docs for arbitrary generic types * ✅ Add tests for generic sub-type validation * 📝 Update change note. Generic support is not so "basic" now * 📝 Update docs with code review * ♻️ Update fields module with code review changes * ✅ Update tests from code review * 📝 Update example for generics, try to simplify and explain better * tweak docs example Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
Change Summary
This adds support for generics that implement
__get_validators__
or whenarbitrary_types_allowed
.The generic type parameters are saved in
field.sub_fields
and custom validations using those sub-types are documented.This would fix/related to #1158
Related issue number
#1158
Checklist
changes/<pull request or issue id>-<github username>.md
file added describing change(see changes/README.md for details)