import asyncio import inspect import sys from pathlib import Path from typing import List from unittest.mock import ANY import pytest 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') def test_args(): @validate_arguments def foo(a: int, b: int): return f'{a}, {b}' assert foo(1, 2) == '1, 2' assert foo(*[1, 2]) == '1, 2' assert foo(*(1, 2)) == '1, 2' assert foo(*[1], 2) == '1, 2' with pytest.raises(ValidationError) as exc_info: foo() assert exc_info.value.errors() == [ {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}, {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, ] with pytest.raises(ValidationError) as exc_info: foo(1, 'x') assert exc_info.value.errors() == [ {'loc': ('b',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, 3) assert exc_info.value.errors() == [ {'loc': ('args',), 'msg': '2 positional arguments expected but 3 given', 'type': 'type_error'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, apple=3) assert exc_info.value.errors() == [ {'loc': ('kwargs',), 'msg': "unexpected keyword argument: 'apple'", 'type': 'type_error'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, a=3) assert exc_info.value.errors() == [ {'loc': ('v__duplicate_kwargs',), 'msg': "multiple values for argument: 'a'", 'type': 'type_error'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, a=3, b=4) assert exc_info.value.errors() == [ {'loc': ('v__duplicate_kwargs',), 'msg': "multiple values for arguments: 'a', 'b'", 'type': 'type_error'} ] def test_wrap(): @validate_arguments def foo_bar(a: int, b: int): """This is the foo_bar method.""" return f'{a}, {b}' assert foo_bar.__doc__ == 'This is the foo_bar method.' 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.vd, ValidatedFunction) assert callable(foo_bar.raw_function) 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', 'v__duplicate_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)) == '' def test_kwargs(): @validate_arguments def foo(*, a: int, b: int): return a + b assert foo.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs'} assert foo(a=1, b=3) == 4 with pytest.raises(ValidationError) as exc_info: foo(a=1, b='x') assert exc_info.value.errors() == [ {'loc': ('b',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 'x') assert exc_info.value.errors() == [ {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}, {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}, {'loc': ('args',), 'msg': '0 positional arguments expected but 2 given', 'type': 'type_error'}, ] def test_untyped(): @validate_arguments def foo(a, b, c='x', *, d='y'): return ', '.join(str(arg) for arg in [a, b, c, d]) assert foo(1, 2) == '1, 2, x, y' assert foo(1, {'x': 2}, c='3', d='4') == "1, {'x': 2}, 3, 4" @pytest.mark.parametrize('validated', (True, False)) def test_var_args_kwargs(validated): def foo(a, b, *args, d=3, **kwargs): return f'a={a!r}, b={b!r}, args={args!r}, d={d!r}, kwargs={kwargs!r}' if validated: foo = validate_arguments(foo) assert foo(1, 2) == 'a=1, b=2, args=(), d=3, kwargs={}' assert foo(1, 2, 3, d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}' assert foo(*[1, 2, 3], d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}' assert foo(1, 2, args=(10, 11)) == "a=1, b=2, args=(), d=3, kwargs={'args': (10, 11)}" assert foo(1, 2, 3, args=(10, 11)) == "a=1, b=2, args=(3,), d=3, kwargs={'args': (10, 11)}" assert foo(1, 2, 3, e=10) == "a=1, b=2, args=(3,), d=3, kwargs={'e': 10}" assert foo(1, 2, kwargs=4) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4}" 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( # language=Python """ from pydantic import validate_arguments @validate_arguments def foo(a, b, /, c=None): return f'{a}, {b}, {c}' """ ) assert module.foo(1, 2) == '1, 2, None' assert module.foo(1, 2, 44) == '1, 2, 44' assert module.foo(1, 2, c=44) == '1, 2, 44' with pytest.raises(ValidationError) as exc_info: module.foo(1, b=2) assert exc_info.value.errors() == [ { 'loc': ('v__positional_only',), 'msg': "positional-only argument passed as keyword argument: 'b'", 'type': 'type_error', } ] with pytest.raises(ValidationError) as exc_info: module.foo(a=1, b=2) assert exc_info.value.errors() == [ { 'loc': ('v__positional_only',), 'msg': "positional-only arguments passed as keyword arguments: 'a', 'b'", 'type': 'type_error', } ] def test_args_name(): @validate_arguments def foo(args: int, kwargs: int): return f'args={args!r}, kwargs={kwargs!r}' assert foo.model.__fields__.keys() == {'args', 'kwargs', 'v__args', 'v__kwargs', 'v__duplicate_kwargs'} assert foo(1, 2) == 'args=1, kwargs=2' with pytest.raises(ValidationError) as exc_info: foo(1, 2, apple=4) assert exc_info.value.errors() == [ {'loc': ('v__kwargs',), 'msg': "unexpected keyword argument: 'apple'", 'type': 'type_error'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, apple=4, banana=5) assert exc_info.value.errors() == [ {'loc': ('v__kwargs',), 'msg': "unexpected keyword arguments: 'apple', 'banana'", 'type': 'type_error'} ] with pytest.raises(ValidationError) as exc_info: foo(1, 2, 3) assert exc_info.value.errors() == [ {'loc': ('v__args',), 'msg': '2 positional arguments expected but 3 given', 'type': 'type_error'} ] def test_v_args(): with pytest.raises( ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted' ): @validate_arguments def foo1(v__args: int): pass with pytest.raises( ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted' ): @validate_arguments def foo2(v__kwargs: int): pass with pytest.raises( ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted' ): @validate_arguments def foo3(v__positional_only: int): pass with pytest.raises( ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted' ): @validate_arguments def foo4(v__duplicate_kwargs: int): pass def test_async(): @validate_arguments async def foo(a, b): return f'a={a} b={b}' async def run(): v = await foo(1, 2) assert v == 'a=1 b=2' loop = asyncio.get_event_loop() loop.run_until_complete(run()) with pytest.raises(ValidationError) as exc_info: loop.run_until_complete(foo('x')) assert exc_info.value.errors() == [{'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'}] def test_string_annotation(): @validate_arguments def foo(a: 'List[int]', b: 'Path'): return f'a={a!r} b={b!r}' assert foo([1, 2, 3], '/') with pytest.raises(ValidationError) as exc_info: foo(['x']) assert exc_info.value.errors() == [ {'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'}, ] 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'}, }, ] def test_validate(mocker): stub = mocker.stub(name='on_something_stub') @validate_arguments def func(s: str, count: int, *, separator: bytes = b''): stub(s, count, separator) func.validate('qwe', 2) with pytest.raises(ValidationError): func.validate(['qwe'], 2) stub.assert_not_called()