diff --git a/pydantic/types.py b/pydantic/types.py index 92ea8f2..2090e92 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -15,6 +15,7 @@ from typing import ( Optional, Pattern, Set, + Tuple, Type, TypeVar, Union, @@ -132,7 +133,7 @@ T = TypeVar('T') class ConstrainedList(list): # type: ignore # Needed for pydantic to detect that this is a list __origin__ = list - __args__: List[Type[T]] # type: ignore + __args__: Tuple[Type[T], ...] # type: ignore min_items: Optional[int] = None max_items: Optional[int] = None @@ -165,7 +166,7 @@ class ConstrainedList(list): # type: ignore def conlist(item_type: Type[T], *, min_items: int = None, max_items: int = None) -> Type[List[T]]: # __args__ is needed to conform to typing generics api - namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]} + namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': (item_type,)} # We use new_class to be able to deal with Generic types return new_class('ConstrainedListValue', (ConstrainedList,), {}, lambda ns: ns.update(namespace)) diff --git a/pydantic/typing.py b/pydantic/typing.py index d204f8f..e71228f 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -124,12 +124,8 @@ else: get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) get_args(Callable[[], T][int]) == ([], int) """ - try: - args = typing_get_args(tp) - except IndexError: - args = () # the fallback is needed for the same reasons as `get_origin` (see above) - return args or getattr(tp, '__args__', ()) or generic_get_args(tp) + return typing_get_args(tp) or getattr(tp, '__args__', ()) or generic_get_args(tp) if TYPE_CHECKING: diff --git a/tests/test_utils.py b/tests/test_utils.py index e3db6be..8dd736c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,18 +1,28 @@ import os import re import string +import sys from copy import copy, deepcopy from distutils.version import StrictVersion from enum import Enum -from typing import NewType, Union +from typing import Callable, Dict, List, NewType, Tuple, TypeVar, Union import pytest -from pydantic import VERSION, BaseModel +from pydantic import VERSION, BaseModel, ConstrainedList, conlist from pydantic.color import Color from pydantic.dataclasses import dataclass from pydantic.fields import Undefined -from pydantic.typing import Literal, all_literal_values, display_as_type, is_new_type, new_type_supertype +from pydantic.typing import ( + ForwardRef, + Literal, + all_literal_values, + display_as_type, + get_args, + is_new_type, + new_type_supertype, + resolve_annotations, +) from pydantic.utils import ( BUILTIN_COLLECTIONS, ClassAttribute, @@ -409,3 +419,30 @@ def test_smart_deepcopy_collection(collection, mocker): expected_value = object() mocker.patch('pydantic.utils.deepcopy', return_value=expected_value) assert smart_deepcopy(collection) is expected_value + + +T = TypeVar('T') + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason='get_args is only consistent for python >= 3.8') +@pytest.mark.parametrize( + 'input_value,output_value', + [ + (conlist(str), (str,)), + (ConstrainedList, ()), + (List[str], (str,)), + (Dict[str, int], (str, int)), + (int, ()), + (Union[int, Union[T, int], str][int], (int, str)), + (Union[int, Tuple[T, int]][str], (int, Tuple[str, int])), + (Callable[[], T][int], ([], int)), + ], +) +def test_get_args(input_value, output_value): + assert get_args(input_value) == output_value + + +def test_resolve_annotations_no_module(): + # TODO: is there a better test for this, can this case really happen? + fr = ForwardRef('Foo') + assert resolve_annotations({'Foo': ForwardRef('Foo')}, None) == {'Foo': fr}