Support Field(default_factory) in validate_arguments (#2176)

* Support Field(default_factory) in validate_arguments

* Added docs for validate_arguments with Field
This commit is contained in:
Tom Cobb
2021-02-23 12:07:11 +00:00
committed by GitHub
parent 7da04d95fa
commit 3f849a368f
5 changed files with 61 additions and 5 deletions
+2
View File
@@ -0,0 +1,2 @@
Allow `Field` with a `default_factory` to be used as an argument to a function
decorated with `validate_arguments`
@@ -0,0 +1,22 @@
from datetime import datetime
from pydantic import validate_arguments, Field, ValidationError
from pydantic.typing import Annotated
@validate_arguments
def how_many(num: Annotated[int, Field(gt=10)]):
return num
try:
how_many(1)
except ValidationError as e:
print(e)
@validate_arguments
def when(dt: datetime = Field(default_factory=datetime.now)):
return dt
print(type(when()))
+15 -3
View File
@@ -55,6 +55,18 @@ To demonstrate all the above parameter types:
```
_(This script is complete, it should run "as is")_
## Using Field to describe function arguments
[Field](schema.md#field-customisation) can also be used with `validate_arguments` to provide extra information about
the field and validations. In general it should be used in a type hint with
[Annotated](schema.md#typingannotated-fields), unless `default_factory` is specified, in which case it should be used
as the default value of the field:
```py
{!.tmp_examples/validation_decorator_field.py!}
```
_(This script is complete, it should run "as is")_
## Usage with mypy
The `validate_arguments` decorator should work "out of the box" with [mypy](http://mypy-lang.org/) since it's
@@ -93,13 +105,13 @@ _(This script is complete, it should run "as is")_
## Custom Config
The model behind `validate_arguments` can be customised using a config setting which is equivalent to
The model behind `validate_arguments` can be customised using a config setting which is equivalent to
setting the `Config` sub-class in normal models.
!!! warning
The `fields` and `alias_generator` properties of `Config` which allow aliases to be configured are not supported
yet with `@validate_arguments`, using them will raise an error.
Configuration is set using the `config` keyword argument to the decorator, it may be either a config class
or a dict of properties which are converted to a class later.
@@ -154,7 +166,7 @@ in future.
### Config and Validators
`fields` and `alias_generator` on custom [`Config`](model_config.md) are not supported, see [above](#custom-config).
Neither are [validators](validators.md).
### Model fields and reserved arguments
+1 -1
View File
@@ -184,7 +184,7 @@ class ValidatedFunction:
return values
def execute(self, m: BaseModel) -> Any:
d = {k: v for k, v in m._iter() if k in m.__fields_set__}
d = {k: v for k, v in m._iter() if k in m.__fields_set__ or m.__fields__[k].default_factory}
var_kwargs = d.pop(self.v_kwargs_name, {})
if self.v_args_name in d:
+21 -1
View File
@@ -3,12 +3,14 @@ import inspect
import sys
from pathlib import Path
from typing import List
from unittest.mock import ANY
import pytest
from pydantic import BaseModel, ValidationError, validate_arguments
from pydantic import BaseModel, Field, ValidationError, validate_arguments
from pydantic.decorator import ValidatedFunction
from pydantic.errors import ConfigError
from pydantic.typing import Annotated
skip_pre_38 = pytest.mark.skipif(sys.version_info < (3, 8), reason='testing >= 3.8 behaviour only')
@@ -142,6 +144,24 @@ def test_var_args_kwargs(validated):
assert foo(1, 2, kwargs=4, e=5) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4, 'e': 5}"
def test_field_can_provide_factory() -> None:
@validate_arguments
def foo(a: int, b: int = Field(default_factory=lambda: 99), *args: int) -> int:
"""mypy is happy with this"""
return a + b + sum(args)
assert foo(3) == 102
assert foo(1, 2, 3) == 6
@pytest.mark.skipif(not Annotated, reason='typing_extensions not installed')
def test_annotated_field_can_provide_factory() -> None:
@validate_arguments
def foo2(a: int, b: Annotated[int, Field(default_factory=lambda: 99)] = ANY, *args: int) -> int:
"""mypy reports Incompatible default for argument "b" if we don't supply ANY as default"""
return a + b + sum(args)
@skip_pre_38
def test_positional_only(create_module):
module = create_module(