1"""
2    test_util_typing
3    ~~~~~~~~~~~~~~~~
4
5    Tests util.typing functions.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import sys
12from numbers import Integral
13from struct import Struct
14from typing import (Any, Callable, Dict, Generator, List, NewType, Optional, Tuple, TypeVar,
15                    Union)
16
17import pytest
18
19from sphinx.util.typing import restify, stringify
20
21
22class MyClass1:
23    pass
24
25
26class MyClass2(MyClass1):
27    __qualname__ = '<MyClass2>'
28
29
30T = TypeVar('T')
31MyInt = NewType('MyInt', int)
32
33
34class MyList(List[T]):
35    pass
36
37
38class BrokenType:
39    __args__ = int
40
41
42def test_restify():
43    assert restify(int) == ":class:`int`"
44    assert restify(str) == ":class:`str`"
45    assert restify(None) == ":obj:`None`"
46    assert restify(Integral) == ":class:`numbers.Integral`"
47    assert restify(Struct) == ":class:`struct.Struct`"
48    assert restify(Any) == ":obj:`Any`"
49
50
51def test_restify_type_hints_containers():
52    assert restify(List) == ":class:`List`"
53    assert restify(Dict) == ":class:`Dict`"
54    assert restify(List[int]) == ":class:`List`\\ [:class:`int`]"
55    assert restify(List[str]) == ":class:`List`\\ [:class:`str`]"
56    assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]"
57    assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]"
58    assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]"
59    assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]"
60    assert restify(MyList[Tuple[int, int]]) == ":class:`tests.test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]"
61    assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]"
62
63
64def test_restify_type_hints_Callable():
65    assert restify(Callable) == ":class:`Callable`"
66
67    if sys.version_info >= (3, 7):
68        assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]"
69        assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]"
70    else:
71        assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]"
72        assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]"
73
74
75def test_restify_type_hints_Union():
76    assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]"
77    assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]"
78    assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]"
79
80    if sys.version_info >= (3, 7):
81        assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]"
82        assert (restify(Union[MyClass1, MyClass2]) ==
83                ":obj:`Union`\\ [:class:`tests.test_util_typing.MyClass1`, :class:`tests.test_util_typing.<MyClass2>`]")
84    else:
85        assert restify(Union[int, Integral]) == ":class:`numbers.Integral`"
86        assert restify(Union[MyClass1, MyClass2]) == ":class:`tests.test_util_typing.MyClass1`"
87
88
89@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
90def test_restify_type_hints_typevars():
91    T = TypeVar('T')
92    T_co = TypeVar('T_co', covariant=True)
93    T_contra = TypeVar('T_contra', contravariant=True)
94
95    assert restify(T) == ":obj:`tests.test_util_typing.T`"
96    assert restify(T_co) == ":obj:`tests.test_util_typing.T_co`"
97    assert restify(T_contra) == ":obj:`tests.test_util_typing.T_contra`"
98    assert restify(List[T]) == ":class:`List`\\ [:obj:`tests.test_util_typing.T`]"
99    assert restify(MyInt) == ":class:`MyInt`"
100
101
102def test_restify_type_hints_custom_class():
103    assert restify(MyClass1) == ":class:`tests.test_util_typing.MyClass1`"
104    assert restify(MyClass2) == ":class:`tests.test_util_typing.<MyClass2>`"
105
106
107def test_restify_type_hints_alias():
108    MyStr = str
109    MyTuple = Tuple[str, str]
110    assert restify(MyStr) == ":class:`str`"
111    assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]"  # type: ignore
112
113
114@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
115def test_restify_type_ForwardRef():
116    from typing import ForwardRef  # type: ignore
117    assert restify(ForwardRef("myint")) == ":class:`myint`"
118
119
120@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
121def test_restify_type_union_operator():
122    assert restify(int | None) == "Optional[:class:`int`]"  # type: ignore
123    assert restify(int | str) == ":class:`int` | :class:`str`"  # type: ignore
124    assert restify(int | str | None) == "Optional[:class:`int` | :class:`str`]"  # type: ignore
125
126
127def test_restify_broken_type_hints():
128    assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'
129
130
131def test_stringify():
132    assert stringify(int) == "int"
133    assert stringify(str) == "str"
134    assert stringify(None) == "None"
135    assert stringify(Integral) == "numbers.Integral"
136    assert restify(Struct) == ":class:`struct.Struct`"
137    assert stringify(Any) == "Any"
138
139
140def test_stringify_type_hints_containers():
141    assert stringify(List) == "List"
142    assert stringify(Dict) == "Dict"
143    assert stringify(List[int]) == "List[int]"
144    assert stringify(List[str]) == "List[str]"
145    assert stringify(Dict[str, float]) == "Dict[str, float]"
146    assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]"
147    assert stringify(Tuple[str, ...]) == "Tuple[str, ...]"
148    assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
149    assert stringify(MyList[Tuple[int, int]]) == "tests.test_util_typing.MyList[Tuple[int, int]]"
150    assert stringify(Generator[None, None, None]) == "Generator[None, None, None]"
151
152
153@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
154def test_stringify_Annotated():
155    from typing import Annotated  # type: ignore
156    assert stringify(Annotated[str, "foo", "bar"]) == "str"  # NOQA
157
158
159def test_stringify_type_hints_string():
160    assert stringify("int") == "int"
161    assert stringify("str") == "str"
162    assert stringify(List["int"]) == "List[int]"
163    assert stringify("Tuple[str]") == "Tuple[str]"
164    assert stringify("unknown") == "unknown"
165
166
167def test_stringify_type_hints_Callable():
168    assert stringify(Callable) == "Callable"
169
170    if sys.version_info >= (3, 7):
171        assert stringify(Callable[[str], int]) == "Callable[[str], int]"
172        assert stringify(Callable[..., int]) == "Callable[[...], int]"
173    else:
174        assert stringify(Callable[[str], int]) == "Callable[str, int]"
175        assert stringify(Callable[..., int]) == "Callable[..., int]"
176
177
178def test_stringify_type_hints_Union():
179    assert stringify(Optional[int]) == "Optional[int]"
180    assert stringify(Union[str, None]) == "Optional[str]"
181    assert stringify(Union[int, str]) == "Union[int, str]"
182
183    if sys.version_info >= (3, 7):
184        assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]"
185        assert (stringify(Union[MyClass1, MyClass2]) ==
186                "Union[tests.test_util_typing.MyClass1, tests.test_util_typing.<MyClass2>]")
187    else:
188        assert stringify(Union[int, Integral]) == "numbers.Integral"
189        assert stringify(Union[MyClass1, MyClass2]) == "tests.test_util_typing.MyClass1"
190
191
192def test_stringify_type_hints_typevars():
193    T = TypeVar('T')
194    T_co = TypeVar('T_co', covariant=True)
195    T_contra = TypeVar('T_contra', contravariant=True)
196
197    assert stringify(T) == "T"
198    assert stringify(T_co) == "T_co"
199    assert stringify(T_contra) == "T_contra"
200    assert stringify(List[T]) == "List[T]"
201    assert stringify(MyInt) == "MyInt"
202
203
204def test_stringify_type_hints_custom_class():
205    assert stringify(MyClass1) == "tests.test_util_typing.MyClass1"
206    assert stringify(MyClass2) == "tests.test_util_typing.<MyClass2>"
207
208
209def test_stringify_type_hints_alias():
210    MyStr = str
211    MyTuple = Tuple[str, str]
212    assert stringify(MyStr) == "str"
213    assert stringify(MyTuple) == "Tuple[str, str]"  # type: ignore
214
215
216@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
217def test_stringify_type_union_operator():
218    assert stringify(int | None) == "Optional[int]"  # type: ignore
219    assert stringify(int | str) == "int | str"  # type: ignore
220    assert stringify(int | str | None) == "Optional[int | str]"  # type: ignore
221
222
223def test_stringify_broken_type_hints():
224    assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType'
225