-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
V2: The validate
general method
#4669
Comments
validate
general method`validate
general method
validate
general methodvalidate
general method
So usage would be from pydantic import validate, BaseModel
@validate(...config)
def my_method(...):
...
@validate(...config)
@dataclass
class MyDataclass:
...
@validate(...config)
class MyTypeDict(TypedDict):
...
ValidatedTuple = validate(tuple[int, int, int])
ValidatedIntStr = validate(int | str)
class Cat(BaseModel):
pet_type: Literal['cat']
class Dog(BaseModel):
pet_type: Literal['dig']
ValidatedPet = validate(PetCat | Dog, descriminator='pet_name') Main question: How do we define validators, do we continue to extract them as methods, or do we let them be provided as kwargs to Updated, added more examples. |
Is this issue still open? if so I was wondering if it would be a reasonable first contribution for me and my partner @smhavens. thanks! |
I'm afraid this is probably quite a challenging first task. How about something like #4675? It's still pretty hard and complicated, but not as wide ranging as this. |
Q: Would the |
Yes, unless you use strict mode. |
(added a few more examples in the above usage example) So the question is, what does Well, it has to depend on what it's called with:
In all these cases, we should hopefully attach enough information for the following:
|
For non-models we currently support or plan to support two approaches: from typing import Annotated
from annotated_types import Predicate
from pydantic import Field
NonNegativeInt = Annotated[int, Field(ge=0)]
EvenInt = Annotated[int, Predicate(lambda x: x % 2 == 0)] The first one already works with So I think we should make from functools import wraps
import inspect
from typing import Annotated, Any, TypeVar
T = TypeVar("T")
def validate(__type_or_func: T, *args: Any, **kwargs: Any) -> T:
if inspect.isfunction(__type_or_func) or inspect.ismethod(__type_or_func):
@wraps(__type_or_func)
def wrapped(*args: Any, **kwargs: Any) -> Any:
# do some validation
return __type_or_func(*args, **kwargs)
return wrapped # type: ignore
else:
return Annotated[__type_or_func, "some metadata"] # type: ignore
# tests
from dataclasses import dataclass
from typing import Literal, TypedDict
from pydantic import BaseModel
@validate
def my_func(a: int) -> int:
return a
_1: int = my_func(1)
@validate
@dataclass
class Foo:
a: int
_2: Foo = Foo(123)
@validate
class MyDict(TypedDict):
a: int
_3: MyDict = {"a": 123}
_4: MyDict = MyDict(a=123)
ValidatedTuple = validate(tuple[int, int, int])
ValidatedIntStr = validate(int | str)
_5: ValidatedTuple = (1, 2, 3)
_6: ValidatedTuple = ValidatedTuple([1, 2, 3])
_7: ValidatedIntStr = 1
_8: ValidatedIntStr = "1"
class Cat(BaseModel):
pet_type: Literal['cat']
class Dog(BaseModel):
pet_type: Literal['dig']
ValidatedPet = validate(Cat | Dog, descriminator='pet_name')
_9: ValidatedPet = Cat(pet_type="cat") This doesn't seem to work with unions. I think Pylance special cases unions because the result is not a type, it's some sort of "special form". We could just say you need to do This also does not make |
I agree with most of your examples. We should definitely make it explicit that we're return a new thing, specifically an instance of With that a user could do validate_pet = Validate(Cat | Dog, descriminator='pet_name')
cat: Cat = validate_pet({'pet_type': 'cat'})
validate_pet.whatever_we_call_to_json(cat) With that, the general usage would be:
On the point of how to define validators, we should support:
WDYT? |
I think this would be a bit problematic: it would erase the original type and using an instance of from dataclasses import dataclass
from typing import Annotated, Any, Callable, Generic, TypeVar, ParamSpec, reveal_type
P = ParamSpec("P")
T = TypeVar("T")
class Validate(Generic[P, T]):
def __init__(self, __thing: Callable[P, T], *args: Any, **kwargs: Any) -> None:
...
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
...
def validate(__thing: Callable[P, T], *args: Any, **kwargs: Any) -> Validate[P, T]:
return Annotated[__thing, "metadata"] # type: ignore
@validate
@dataclass
class MyCls:
a: int
reveal_type(MyCls(a=1)) # MyCls
reveal_type(MyCls) # Validate[(*args: Any, **kwargs: Any), MyCls]
# Expected type expression but received "Validate[(*args: Any, **kwargs: Any), MyCls]"
def foo(thing: MyCls) -> None:
pass So I don't think @validated(after=lambda x: x.a > 2)
@dataclass
class MyCls:
a: int
my_cls_validator = Validate(MyCls)
reveal_type(MyCls(a=1)) # MyCls
reveal_type(MyCls) # type[MyCls]
def foo(thing: MyCls) -> None:
pass
foo(my_cls_validator({"a": "3"}))
my_cls_validator({"a": "1"}) # fails
reveal_type(my_cls_validator({"a": "1"}) # MyCls But if used in a validated context, you would never need to create a @app.get("/foo")
def endpoint(thing: MyCls): # FastAPI creates the Validate instance internally
...
class MyModel(BaseModel):
field: MyCls # MyCls' validation logic is applied to this field |
Yeah this sounds good to me. Something like: from pydantic.validators import AfterValidator, WrapValidator, PlainValidator
Number = float | int
ValidatedNumberWrapped = Annoated[Number, WrapValidator(Number, lambda v, handler: handler(v))]
ValidatedNumberAfter = Annoated[Number, AfterValidator(Number, lambda v: v)]
ValidatedNumberPlain = Annoated[Number, PlainValidator(Number, lambda v: v)] Note that I'm requiring that
I'm not sure I'm understanding what is special about
The only difference is that
Yes agreed: @dataclass
class MyCls:
a: int
@validator("a", mode="wrap")
def validate_a(cls, v: int, handler: Callable[[int], int]) -> int:
return handler(v) Although part of me wants there to be only "one way" to do this (to do
Yep that's the idea 👍🏻 . |
What semantics do we expect from typing import List
from pydantic import BaseModel
IntList = List[int]
OuterList = List["IntList"]
class MyModel(BaseModel):
x: OuterList This works on both V1 and V2. It works because Take the same situation with from typing import List
from pydantic import Validator
IntList = List[int]
OuterList = List["IntList"]
Validator(OuterList) I think in this case it makes sense for it to work the same as a model. But I expect IntList = List[int]
OuterList = List["IntList"]
async def endpoint(body: OuterList): ... In this case I'd expect from typing import List
from pydantic import create_model
IntList = List[int]
OuterList = List["IntList"]
MyModel = create_model("MyModel", x=(OuterList, ...))
MyModel.update_forward_refs() If you pass in from typing import List
from pydantic import create_model
IntList = List[int]
OuterList = List["IntList"]
MyModel = create_model("MyModel", __module__=__name__, x=(OuterList, ...))
MyModel.update_forward_refs() So I think what we should do for from typing import List
from pydantic import Validator
IntList = List[int]
OuterList = List["IntList"]
Validator(OuterList, globalns=globals(), localns=locals()) |
I think if, in practice, there is some significant fraction of usages of (As soon as I see APIs where I am forced to pass a namespace in, my eyes glaze over and I assume that I will need to put in more effort to understand what is required than I want to. On the other hand, if I see APIs where I have the option to pass in a namespace but it "just works" if I don't, I proceed with much less fear.) As a pydantic user I have frequently found Either way, I agree that it makes sense to allow namespace overrides for exactly the reasons indicated above. |
I haven't used from typing import List
from pydantic import parse_obj_as
IntList = List[int]
OuterList = List["IntList"]
parse_obj_as(OuterList, [[1]])
So this doesn't seem to work at all with |
@dmontagu pointed out that we can use |
I wish I had a stronger/useful opinion but I don't. I'm not really sure of the advantages/disadvantages down the line. 🤔 I suspect that stringified forward references will be much less needed/problematic after PEP 649 (which seems like it's probably what's gonna be accepted). But that solves mainly for places that take only type annotations, not that much for Python expressions. Not even sure if that's irrelevant here. I'm not sure I have a lot of clarity about the difference in semantics between For FastAPI, it's true that it's important to be able to define in function parameters Pydantic fields, not models, to allow all the extra types (e.g. I agree that something decorated with Sorry if I'm misunderstanding something and talking nonsense/unrelated stuff in some of the points above. 😅 Thanks @adriangb for pinging me in DM! (I wouldn't have noticed a tag in GitHub, I have too many GitHub notifications). |
I'll just note that thanks to improvements in mypy, specifically with improved support for decorator typing, I suspect you could get this to type-check properly. That said, I don't have a strong opinion about what the best approach is; not trying to argue that we should go down that route. |
I think what we have now with |
Pydantic V2 will do a massively better job of validating arbitrary objects.
To accomplish this without many methods, we should provide one function which can:
dataclass
,NamedTuple
,TypedDict
ValidatedTuple = validate(tuple[int, int, int])
With this, the pydantic version of the
dataclass
decorator, will just become effectively:Dataclasses need work (#4670), but validation and generate validation schemas for the rest of these types should already work.
The text was updated successfully, but these errors were encountered: