diff --git a/python-pydantic.changes b/python-pydantic.changes index 231f27d..98394a3 100644 --- a/python-pydantic.changes +++ b/python-pydantic.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Mon Oct 12 08:58:44 UTC 2020 - Marketa Calabkova + +- Add upstream patch validate-config.patch which fixes build with new pytest + ------------------------------------------------------------------- Sun Aug 2 15:14:47 UTC 2020 - John Vandenberg diff --git a/python-pydantic.spec b/python-pydantic.spec index eaf24f1..4be70c8 100644 --- a/python-pydantic.spec +++ b/python-pydantic.spec @@ -27,6 +27,8 @@ License: MIT Group: Development/Languages/Python URL: https://github.com/samuelcolvin/pydantic Source: https://github.com/samuelcolvin/pydantic/archive/v%{version}.tar.gz#/pydantic-%{version}.tar.gz +# PATCH-FIX-UPSTREAM https://github.com/samuelcolvin/pydantic/commit/9c4860ce964a4eb2e22eedc21f21d406c596a82f Valdiate arguments config (#1663) +Patch0: validate-config.patch BuildRequires: %{python_module email_validator >= 1.0.3} BuildRequires: %{python_module mypy} BuildRequires: %{python_module pytest} @@ -46,6 +48,7 @@ Data validation and settings management using Python type hinting. %prep %setup -q -n pydantic-%{version} +%patch0 -p1 sed -i /dataclasses/d setup.py %build diff --git a/validate-config.patch b/validate-config.patch new file mode 100644 index 0000000..52982f2 --- /dev/null +++ b/validate-config.patch @@ -0,0 +1,304 @@ +From 9c4860ce964a4eb2e22eedc21f21d406c596a82f Mon Sep 17 00:00:00 2001 +From: Samuel Colvin +Date: Sun, 6 Sep 2020 23:17:52 +0100 +Subject: [PATCH] Valdiate arguments config (#1663) + +* add `configs` to validate_arguments + +* simplify `validate_arguments` and add annotation for parameter `configs` + +* change double quotes to single quotes + +* reformat code + +* fix mypy error + +* fix mypy 'maximum semantic analysis' error + +* rename 'configs' > 'config_params' + +* change name and usage, start tests + +* prevent setting fields on custom config + +* add docs and fix mypy + +* tweak docs + +* add change + +Co-authored-by: quantpy +--- + changes/1663-samuelcolvin.md | 1 + + docs/examples/validation_decorator_config.py | 26 +++++++++ + docs/usage/validation_decorator.md | 21 +++++++- + pydantic/decorator.py | 51 +++++++++++++----- + setup.cfg | 1 - + tests/test_decorator.py | 55 ++++++++++++++++++++ + 6 files changed, 138 insertions(+), 17 deletions(-) + create mode 100644 changes/1663-samuelcolvin.md + create mode 100644 docs/examples/validation_decorator_config.py + +Index: pydantic-1.6.1/changes/1663-samuelcolvin.md +=================================================================== +--- /dev/null ++++ pydantic-1.6.1/changes/1663-samuelcolvin.md +@@ -0,0 +1 @@ ++add `config` to `@validate_arguments` +Index: pydantic-1.6.1/docs/examples/validation_decorator_config.py +=================================================================== +--- /dev/null ++++ pydantic-1.6.1/docs/examples/validation_decorator_config.py +@@ -0,0 +1,26 @@ ++from pydantic import ValidationError, validate_arguments ++ ++ ++class Foobar: ++ def __init__(self, v: str): ++ self.v = v ++ ++ def __add__(self, other: 'Foobar') -> str: ++ return f'{self} + {other}' ++ ++ def __str__(self) -> str: ++ return f'Foobar({self.v})' ++ ++ ++@validate_arguments(config=dict(arbitrary_types_allowed=True)) ++def add_foobars(a: Foobar, b: Foobar): ++ return a + b ++ ++ ++c = add_foobars(Foobar('a'), Foobar('b')) ++print(c) ++ ++try: ++ add_foobars(1, 2) ++except ValidationError as e: ++ print(e) +Index: pydantic-1.6.1/docs/usage/validation_decorator.md +=================================================================== +--- pydantic-1.6.1.orig/docs/usage/validation_decorator.md ++++ pydantic-1.6.1/docs/usage/validation_decorator.md +@@ -78,8 +78,23 @@ _(This script is complete, it should run + ```py + {!.tmp_examples/validation_decorator_async.py!} + ``` +-_(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 ++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. ++ ++```py ++{!.tmp_examples/validation_decorator_config.py!} ++``` ++_(This script is complete, it should run "as is")_ + + ## Limitations + +@@ -126,7 +141,9 @@ in future. + + ### Config and Validators + +-Custom [`Config`](model_config.md) and [validators](validators.md) are not yet supported. ++`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 + +Index: pydantic-1.6.1/pydantic/decorator.py +=================================================================== +--- pydantic-1.6.1.orig/pydantic/decorator.py ++++ pydantic-1.6.1/pydantic/decorator.py +@@ -1,5 +1,5 @@ + from functools import wraps +-from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, TypeVar, cast, get_type_hints ++from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, Type, TypeVar, Union, cast, get_type_hints + + from . import validator + from .errors import ConfigError +@@ -12,22 +12,30 @@ if TYPE_CHECKING: + from .typing import AnyCallable + + Callable = TypeVar('Callable', bound=AnyCallable) ++ ConfigType = Union[None, Type[Any], Dict[str, Any]] + + +-def validate_arguments(function: 'Callable') -> 'Callable': ++def validate_arguments(func: 'Callable' = None, *, config: 'ConfigType' = None) -> 'Callable': + """ + Decorator to validate the arguments passed to a function. + """ +- vd = ValidatedFunction(function) + +- @wraps(function) +- def wrapper_function(*args: Any, **kwargs: Any) -> Any: +- return vd.call(*args, **kwargs) +- +- wrapper_function.vd = vd # type: ignore +- wrapper_function.raw_function = vd.raw_function # type: ignore +- wrapper_function.model = vd.model # type: ignore +- return cast('Callable', wrapper_function) ++ def validate(_func: 'Callable') -> 'Callable': ++ vd = ValidatedFunction(_func, config) ++ ++ @wraps(_func) ++ def wrapper_function(*args: Any, **kwargs: Any) -> Any: ++ return vd.call(*args, **kwargs) ++ ++ wrapper_function.vd = vd # type: ignore ++ wrapper_function.raw_function = vd.raw_function # type: ignore ++ wrapper_function.model = vd.model # type: ignore ++ return cast('Callable', wrapper_function) ++ ++ if func: ++ return validate(func) ++ else: ++ return cast('Callable', validate) + + + ALT_V_ARGS = 'v__args' +@@ -36,7 +44,7 @@ V_POSITIONAL_ONLY_NAME = 'v__positional_ + + + class ValidatedFunction: +- def __init__(self, function: 'Callable'): ++ def __init__(self, function: 'Callable', config: 'ConfigType'): # noqa C901 + from inspect import signature, Parameter + + parameters: Mapping[str, Parameter] = signature(function).parameters +@@ -100,7 +108,7 @@ class ValidatedFunction: + # same with kwargs + fields[self.v_kwargs_name] = Dict[Any, Any], None + +- self.create_model(fields, takes_args, takes_kwargs) ++ self.create_model(fields, takes_args, takes_kwargs, config) + + def call(self, *args: Any, **kwargs: Any) -> Any: + values = self.build_values(args, kwargs) +@@ -170,9 +178,24 @@ class ValidatedFunction: + else: + return self.raw_function(**d) + +- def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool) -> None: ++ def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None: + pos_args = len(self.arg_mapping) + ++ class CustomConfig: ++ pass ++ ++ if not TYPE_CHECKING: # pragma: no branch ++ if isinstance(config, dict): ++ CustomConfig = type('Config', (), config) # noqa: F811 ++ elif config is not None: ++ CustomConfig = config # noqa: F811 ++ ++ if hasattr(CustomConfig, 'fields') or hasattr(CustomConfig, 'alias_generator'): ++ raise ConfigError( ++ 'Setting the "fields" and "alias_generator" property on custom Config for ' ++ '@validate_arguments is not yet supported, please remove.' ++ ) ++ + class DecoratorBaseModel(BaseModel): + @validator(self.v_args_name, check_fields=False, allow_reuse=True) + def check_args(cls, v: List[Any]) -> List[Any]: +@@ -196,7 +219,7 @@ class ValidatedFunction: + keys = ', '.join(map(repr, v)) + raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}') + +- class Config: ++ class Config(CustomConfig): + extra = Extra.forbid + + self.model = create_model(to_camel(self.raw_function.__name__), __base__=DecoratorBaseModel, **fields) +Index: pydantic-1.6.1/setup.cfg +=================================================================== +--- pydantic-1.6.1.orig/setup.cfg ++++ pydantic-1.6.1/setup.cfg +@@ -1,6 +1,5 @@ + [tool:pytest] + testpaths = tests +-timeout = 10 + filterwarnings = + error + ignore::DeprecationWarning:distutils +Index: pydantic-1.6.1/tests/test_decorator.py +=================================================================== +--- pydantic-1.6.1.orig/tests/test_decorator.py ++++ pydantic-1.6.1/tests/test_decorator.py +@@ -70,6 +70,7 @@ def test_wrap(): + assert issubclass(foo_bar.model, BaseModel) + assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs'} + assert foo_bar.model.__name__ == 'FooBar' ++ assert foo_bar.model.schema()['title'] == 'FooBar' + # signature is slightly different on 3.6 + if sys.version_info >= (3, 7): + assert repr(inspect.signature(foo_bar)) == '' +@@ -262,3 +263,57 @@ def test_class_method(): + {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}, + {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, + ] ++ ++ ++def test_config_title(): ++ @validate_arguments(config=dict(title='Testing')) ++ def foo(a: int, b: int): ++ return f'{a}, {b}' ++ ++ assert foo(1, 2) == '1, 2' ++ assert foo(1, b=2) == '1, 2' ++ assert foo.model.schema()['title'] == 'Testing' ++ ++ ++def test_config_title_cls(): ++ class Config: ++ title = 'Testing' ++ ++ @validate_arguments(config=Config) ++ def foo(a: int, b: int): ++ return f'{a}, {b}' ++ ++ assert foo(1, 2) == '1, 2' ++ assert foo(1, b=2) == '1, 2' ++ assert foo.model.schema()['title'] == 'Testing' ++ ++ ++def test_config_fields(): ++ with pytest.raises(ConfigError, match='Setting the "fields" and "alias_generator" property on custom Config for @'): ++ ++ @validate_arguments(config=dict(fields={'b': 'bang'})) ++ def foo(a: int, b: int): ++ return f'{a}, {b}' ++ ++ ++def test_config_arbitrary_types_allowed(): ++ class EggBox: ++ def __str__(self) -> str: ++ return 'EggBox()' ++ ++ @validate_arguments(config=dict(arbitrary_types_allowed=True)) ++ def foo(a: int, b: EggBox): ++ return f'{a}, {b}' ++ ++ assert foo(1, EggBox()) == '1, EggBox()' ++ with pytest.raises(ValidationError) as exc_info: ++ assert foo(1, 2) == '1, 2' ++ ++ assert exc_info.value.errors() == [ ++ { ++ 'loc': ('b',), ++ 'msg': 'instance of EggBox expected', ++ 'type': 'type_error.arbitrary_type', ++ 'ctx': {'expected_arbitrary_type': 'EggBox'}, ++ }, ++ ]