From 3f849a368f260caab3288f2ad8fe018d7e903bc3 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Tue, 23 Feb 2021 12:07:11 +0000 Subject: [PATCH] Support Field(default_factory) in validate_arguments (#2176) * Support Field(default_factory) in validate_arguments * Added docs for validate_arguments with Field --- changes/2176-thomascobb.md | 2 ++ docs/examples/validation_decorator_field.py | 22 +++++++++++++++++++++ docs/usage/validation_decorator.md | 18 ++++++++++++++--- pydantic/decorator.py | 2 +- tests/test_decorator.py | 22 ++++++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 changes/2176-thomascobb.md create mode 100644 docs/examples/validation_decorator_field.py diff --git a/changes/2176-thomascobb.md b/changes/2176-thomascobb.md new file mode 100644 index 0000000..7ef0f30 --- /dev/null +++ b/changes/2176-thomascobb.md @@ -0,0 +1,2 @@ +Allow `Field` with a `default_factory` to be used as an argument to a function +decorated with `validate_arguments` \ No newline at end of file diff --git a/docs/examples/validation_decorator_field.py b/docs/examples/validation_decorator_field.py new file mode 100644 index 0000000..db8b317 --- /dev/null +++ b/docs/examples/validation_decorator_field.py @@ -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())) diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md index 03578c8..914b931 100644 --- a/docs/usage/validation_decorator.md +++ b/docs/usage/validation_decorator.md @@ -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 diff --git a/pydantic/decorator.py b/pydantic/decorator.py index 383fa2f..933a4d1 100644 --- a/pydantic/decorator.py +++ b/pydantic/decorator.py @@ -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: diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 83d159c..bf8ed83 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -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(