-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for generics with __get_validators__ (#1159)
* ✨ 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>
- Loading branch information
1 parent
be13347
commit aeba494
Showing
7 changed files
with
308 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
add support for generics that implement `__get_validators__` like a custom data type. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from pydantic import BaseModel, ValidationError | ||
|
||
# This is not a pydantic model, it's an arbitrary class | ||
class Pet: | ||
def __init__(self, name: str): | ||
self.name = name | ||
|
||
class Model(BaseModel): | ||
pet: Pet | ||
owner: str | ||
|
||
class Config: | ||
arbitrary_types_allowed = True | ||
|
||
pet = Pet(name='Hedwig') | ||
# A simple check of instance type is used to validate the data | ||
model = Model(owner='Harry', pet=pet) | ||
print(model) | ||
print(model.pet) | ||
print(model.pet.name) | ||
print(type(model.pet)) | ||
try: | ||
# If the value is not an instance of the type, it's invalid | ||
Model(owner='Harry', pet='Hedwig') | ||
except ValidationError as e: | ||
print(e) | ||
# Nothing in the instance of the arbitrary type is checked | ||
# Here name probably should have been a str, but it's not validated | ||
pet2 = Pet(name=42) | ||
model2 = Model(owner='Harry', pet=pet2) | ||
print(model2) | ||
print(model2.pet) | ||
print(model2.pet.name) | ||
print(type(model2.pet)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
from pydantic import BaseModel, ValidationError | ||
from pydantic.fields import ModelField | ||
from typing import TypeVar, Generic | ||
|
||
AgedType = TypeVar('AgedType') | ||
QualityType = TypeVar('QualityType') | ||
|
||
# This is not a pydantic model, it's an arbitrary generic class | ||
class TastingModel(Generic[AgedType, QualityType]): | ||
def __init__(self, name: str, aged: AgedType, quality: QualityType): | ||
self.name = name | ||
self.aged = aged | ||
self.quality = quality | ||
|
||
@classmethod | ||
def __get_validators__(cls): | ||
yield cls.validate | ||
|
||
@classmethod | ||
# You don't need to add the "ModelField", but it will help your | ||
# editor give you completion and catch errors | ||
def validate(cls, v, field: ModelField): | ||
if not isinstance(v, cls): | ||
# The value is not even a TastingModel | ||
raise TypeError('Invalid value') | ||
if not field.sub_fields: | ||
# Generic parameters were not provided so we don't try to validate | ||
# them and just return the value as is | ||
return v | ||
aged_f = field.sub_fields[0] | ||
quality_f = field.sub_fields[1] | ||
errors = [] | ||
# Here we don't need the validated value, but we want the errors | ||
valid_value, error = aged_f.validate(v.aged, {}, loc='aged') | ||
if error: | ||
errors.append(error) | ||
# Here we don't need the validated value, but we want the errors | ||
valid_value, error = quality_f.validate(v.quality, {}, loc='quality') | ||
if error: | ||
errors.append(error) | ||
if errors: | ||
raise ValidationError(errors, cls) | ||
# Validation passed without errors, return the same instance received | ||
return v | ||
|
||
class Model(BaseModel): | ||
# for wine, "aged" is an int with years, "quality" is a float | ||
wine: TastingModel[int, float] | ||
# for cheese, "aged" is a bool, "quality" is a str | ||
cheese: TastingModel[bool, str] | ||
# for thing, "aged" is a Any, "quality" is Any | ||
thing: TastingModel | ||
|
||
model = Model( | ||
# This wine was aged for 20 years and has a quality of 85.6 | ||
wine=TastingModel(name='Cabernet Sauvignon', aged=20, quality=85.6), | ||
# This cheese is aged (is mature) and has "Good" quality | ||
cheese=TastingModel(name='Gouda', aged=True, quality='Good'), | ||
# This Python thing has aged "Not much" and has a quality "Awesome" | ||
thing=TastingModel(name='Python', aged='Not much', quality='Awesome') | ||
) | ||
print(model) | ||
print(model.wine.aged) | ||
print(model.wine.quality) | ||
print(model.cheese.aged) | ||
print(model.cheese.quality) | ||
print(model.thing.aged) | ||
try: | ||
# If the values of the sub-types are invalid, we get an error | ||
Model( | ||
# For wine, aged should be an int with the years, and quality a float | ||
wine=TastingModel(name='Merlot', aged=True, quality='Kinda good'), | ||
# For cheese, aged should be a bool, and quality a str | ||
cheese=TastingModel(name='Gouda', aged='yeah', quality=5), | ||
# For thing, no type parameters are declared, and we skipped validation | ||
# in those cases in the Assessment.validate() function | ||
thing=TastingModel(name='Python', aged='Not much', quality='Awesome') | ||
) | ||
except ValidationError as e: | ||
print(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.