From e57f777c4d5f201e367eed173d02e82a7ddbc59c Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 17 Mar 2020 20:12:30 +0000 Subject: [PATCH] Support instance methods and class methods with @validate_arguments (#1272) --- changes/1222-samuelcolvin.md | 1 + pydantic/decorator.py | 15 ++++++++--- tests/test_decorator.py | 50 +++++++++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 changes/1222-samuelcolvin.md diff --git a/changes/1222-samuelcolvin.md b/changes/1222-samuelcolvin.md new file mode 100644 index 0000000..896fc1e --- /dev/null +++ b/changes/1222-samuelcolvin.md @@ -0,0 +1 @@ +Support instance methods and class methods with `@validate_arguments` diff --git a/pydantic/decorator.py b/pydantic/decorator.py index 94e684f..d99ab1d 100644 --- a/pydantic/decorator.py +++ b/pydantic/decorator.py @@ -1,4 +1,4 @@ -from functools import update_wrapper +from functools import wraps from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, TypeVar, cast, get_type_hints from . import validator @@ -19,8 +19,15 @@ def validate_arguments(function: 'Callable') -> 'Callable': Decorator to validate the arguments passed to a function. """ vd = ValidatedFunction(function) - vd = update_wrapper(vd, function) # type: ignore - return cast('Callable', vd) + + @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) ALT_V_ARGS = 'v__args' @@ -95,7 +102,7 @@ class ValidatedFunction: self.create_model(fields, takes_args, takes_kwargs) - def __call__(self, *args: Any, **kwargs: Any) -> Any: + def call(self, *args: Any, **kwargs: Any) -> Any: values = self.build_values(args, kwargs) m = self.model(**values) return self.execute(m) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 33acd4f..4727961 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -63,10 +63,10 @@ def test_wrap(): assert foo_bar.__name__ == 'foo_bar' assert foo_bar.__module__ == 'tests.test_decorator' assert foo_bar.__qualname__ == 'test_wrap..foo_bar' - assert isinstance(foo_bar, ValidatedFunction) + assert isinstance(foo_bar.vd, ValidatedFunction) assert callable(foo_bar.raw_function) - assert foo_bar.arg_mapping == {0: 'a', 1: 'b'} - assert foo_bar.positional_only_args == set() + assert foo_bar.vd.arg_mapping == {0: 'a', 1: 'b'} + assert foo_bar.vd.positional_only_args == set() assert issubclass(foo_bar.model, BaseModel) assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs'} assert foo_bar.model.__name__ == 'FooBar' @@ -218,3 +218,47 @@ def test_string_annotation(): {'loc': ('a', 0), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}, {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, ] + + +def test_item_method(): + class X: + def __init__(self, v): + self.v = v + + @validate_arguments + def foo(self, a: int, b: int): + assert self.v == a + return f'{a}, {b}' + + x = X(4) + assert x.foo(4, 2) == '4, 2' + assert x.foo(*[4, 2]) == '4, 2' + + with pytest.raises(ValidationError) as exc_info: + x.foo() + + assert exc_info.value.errors() == [ + {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}, + {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, + ] + + +def test_class_method(): + class X: + @classmethod + @validate_arguments + def foo(cls, a: int, b: int): + assert cls == X + return f'{a}, {b}' + + x = X() + assert x.foo(4, 2) == '4, 2' + assert x.foo(*[4, 2]) == '4, 2' + + with pytest.raises(ValidationError) as exc_info: + x.foo() + + assert exc_info.value.errors() == [ + {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}, + {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, + ]