1"""
2Tests for behaviour related to type annotations.
3"""
4
5from sys import version_info
6
7from pyflakes import messages as m
8from pyflakes.test.harness import TestCase, skipIf
9
10
11class TestTypeAnnotations(TestCase):
12
13    def test_typingOverload(self):
14        """Allow intentional redefinitions via @typing.overload"""
15        self.flakes("""
16        import typing
17        from typing import overload
18
19        @overload
20        def f(s):  # type: (None) -> None
21            pass
22
23        @overload
24        def f(s):  # type: (int) -> int
25            pass
26
27        def f(s):
28            return s
29
30        @typing.overload
31        def g(s):  # type: (None) -> None
32            pass
33
34        @typing.overload
35        def g(s):  # type: (int) -> int
36            pass
37
38        def g(s):
39            return s
40        """)
41
42    def test_typingExtensionsOverload(self):
43        """Allow intentional redefinitions via @typing_extensions.overload"""
44        self.flakes("""
45        import typing_extensions
46        from typing_extensions import overload
47
48        @overload
49        def f(s):  # type: (None) -> None
50            pass
51
52        @overload
53        def f(s):  # type: (int) -> int
54            pass
55
56        def f(s):
57            return s
58
59        @typing_extensions.overload
60        def g(s):  # type: (None) -> None
61            pass
62
63        @typing_extensions.overload
64        def g(s):  # type: (int) -> int
65            pass
66
67        def g(s):
68            return s
69        """)
70
71    @skipIf(version_info < (3, 5), 'new in Python 3.5')
72    def test_typingOverloadAsync(self):
73        """Allow intentional redefinitions via @typing.overload (async)"""
74        self.flakes("""
75        from typing import overload
76
77        @overload
78        async def f(s):  # type: (None) -> None
79            pass
80
81        @overload
82        async def f(s):  # type: (int) -> int
83            pass
84
85        async def f(s):
86            return s
87        """)
88
89    def test_overload_with_multiple_decorators(self):
90        self.flakes("""
91            from typing import overload
92            dec = lambda f: f
93
94            @dec
95            @overload
96            def f(x):  # type: (int) -> int
97                pass
98
99            @dec
100            @overload
101            def f(x):  # type: (str) -> str
102                pass
103
104            @dec
105            def f(x): return x
106       """)
107
108    def test_overload_in_class(self):
109        self.flakes("""
110        from typing import overload
111
112        class C:
113            @overload
114            def f(self, x):  # type: (int) -> int
115                pass
116
117            @overload
118            def f(self, x):  # type: (str) -> str
119                pass
120
121            def f(self, x): return x
122        """)
123
124    def test_not_a_typing_overload(self):
125        """regression test for @typing.overload detection bug in 2.1.0"""
126        self.flakes("""
127            def foo(x):
128                return x
129
130            @foo
131            def bar():
132                pass
133
134            def bar():
135                pass
136        """, m.RedefinedWhileUnused)
137
138    @skipIf(version_info < (3, 6), 'new in Python 3.6')
139    def test_variable_annotations(self):
140        self.flakes('''
141        name: str
142        age: int
143        ''')
144        self.flakes('''
145        name: str = 'Bob'
146        age: int = 18
147        ''')
148        self.flakes('''
149        class C:
150            name: str
151            age: int
152        ''')
153        self.flakes('''
154        class C:
155            name: str = 'Bob'
156            age: int = 18
157        ''')
158        self.flakes('''
159        def f():
160            name: str
161            age: int
162        ''')
163        self.flakes('''
164        def f():
165            name: str = 'Bob'
166            age: int = 18
167            foo: not_a_real_type = None
168        ''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable, m.UndefinedName)
169        self.flakes('''
170        def f():
171            name: str
172            print(name)
173        ''', m.UndefinedName)
174        self.flakes('''
175        from typing import Any
176        def f():
177            a: Any
178        ''')
179        self.flakes('''
180        foo: not_a_real_type
181        ''', m.UndefinedName)
182        self.flakes('''
183        foo: not_a_real_type = None
184        ''', m.UndefinedName)
185        self.flakes('''
186        class C:
187            foo: not_a_real_type
188        ''', m.UndefinedName)
189        self.flakes('''
190        class C:
191            foo: not_a_real_type = None
192        ''', m.UndefinedName)
193        self.flakes('''
194        def f():
195            class C:
196                foo: not_a_real_type
197        ''', m.UndefinedName)
198        self.flakes('''
199        def f():
200            class C:
201                foo: not_a_real_type = None
202        ''', m.UndefinedName)
203        self.flakes('''
204        from foo import Bar
205        bar: Bar
206        ''')
207        self.flakes('''
208        from foo import Bar
209        bar: 'Bar'
210        ''')
211        self.flakes('''
212        import foo
213        bar: foo.Bar
214        ''')
215        self.flakes('''
216        import foo
217        bar: 'foo.Bar'
218        ''')
219        self.flakes('''
220        from foo import Bar
221        def f(bar: Bar): pass
222        ''')
223        self.flakes('''
224        from foo import Bar
225        def f(bar: 'Bar'): pass
226        ''')
227        self.flakes('''
228        from foo import Bar
229        def f(bar) -> Bar: return bar
230        ''')
231        self.flakes('''
232        from foo import Bar
233        def f(bar) -> 'Bar': return bar
234        ''')
235        self.flakes('''
236        bar: 'Bar'
237        ''', m.UndefinedName)
238        self.flakes('''
239        bar: 'foo.Bar'
240        ''', m.UndefinedName)
241        self.flakes('''
242        from foo import Bar
243        bar: str
244        ''', m.UnusedImport)
245        self.flakes('''
246        from foo import Bar
247        def f(bar: str): pass
248        ''', m.UnusedImport)
249        self.flakes('''
250        def f(a: A) -> A: pass
251        class A: pass
252        ''', m.UndefinedName, m.UndefinedName)
253        self.flakes('''
254        def f(a: 'A') -> 'A': return a
255        class A: pass
256        ''')
257        self.flakes('''
258        a: A
259        class A: pass
260        ''', m.UndefinedName)
261        self.flakes('''
262        a: 'A'
263        class A: pass
264        ''')
265        self.flakes('''
266        a: 'A B'
267        ''', m.ForwardAnnotationSyntaxError)
268        self.flakes('''
269        a: 'A; B'
270        ''', m.ForwardAnnotationSyntaxError)
271        self.flakes('''
272        a: '1 + 2'
273        ''')
274        self.flakes('''
275        a: 'a: "A"'
276        ''', m.ForwardAnnotationSyntaxError)
277
278    @skipIf(version_info < (3, 5), 'new in Python 3.5')
279    def test_annotated_async_def(self):
280        self.flakes('''
281        class c: pass
282        async def func(c: c) -> None: pass
283        ''')
284
285    @skipIf(version_info < (3, 7), 'new in Python 3.7')
286    def test_postponed_annotations(self):
287        self.flakes('''
288        from __future__ import annotations
289        def f(a: A) -> A: pass
290        class A:
291            b: B
292        class B: pass
293        ''')
294
295        self.flakes('''
296        from __future__ import annotations
297        def f(a: A) -> A: pass
298        class A:
299            b: Undefined
300        class B: pass
301        ''', m.UndefinedName)
302
303    def test_typeCommentsMarkImportsAsUsed(self):
304        self.flakes("""
305        from mod import A, B, C, D, E, F, G
306
307
308        def f(
309            a,  # type: A
310        ):
311            # type: (...) -> B
312            for b in a:  # type: C
313                with b as c:  # type: D
314                    d = c.x  # type: E
315                    return d
316
317
318        def g(x):  # type: (F) -> G
319            return x.y
320        """)
321
322    def test_typeCommentsFullSignature(self):
323        self.flakes("""
324        from mod import A, B, C, D
325        def f(a, b):
326            # type: (A, B[C]) -> D
327            return a + b
328        """)
329
330    def test_typeCommentsStarArgs(self):
331        self.flakes("""
332        from mod import A, B, C, D
333        def f(a, *b, **c):
334            # type: (A, *B, **C) -> D
335            return a + b
336        """)
337
338    def test_typeCommentsFullSignatureWithDocstring(self):
339        self.flakes('''
340        from mod import A, B, C, D
341        def f(a, b):
342            # type: (A, B[C]) -> D
343            """do the thing!"""
344            return a + b
345        ''')
346
347    def test_typeCommentsAdditionalComment(self):
348        self.flakes("""
349        from mod import F
350
351        x = 1 # type: F  # noqa
352        """)
353
354    def test_typeCommentsNoWhitespaceAnnotation(self):
355        self.flakes("""
356        from mod import F
357
358        x = 1  #type:F
359        """)
360
361    def test_typeCommentsInvalidDoesNotMarkAsUsed(self):
362        self.flakes("""
363        from mod import F
364
365        # type: F
366        """, m.UnusedImport)
367
368    def test_typeCommentsSyntaxError(self):
369        self.flakes("""
370        def f(x):  # type: (F[) -> None
371            pass
372        """, m.CommentAnnotationSyntaxError)
373
374    def test_typeCommentsSyntaxErrorCorrectLine(self):
375        checker = self.flakes("""\
376        x = 1
377        # type: definitely not a PEP 484 comment
378        """, m.CommentAnnotationSyntaxError)
379        self.assertEqual(checker.messages[0].lineno, 2)
380
381    def test_typeCommentsAssignedToPreviousNode(self):
382        # This test demonstrates an issue in the implementation which
383        # associates the type comment with a node above it, however the type
384        # comment isn't valid according to mypy.  If an improved approach
385        # which can detect these "invalid" type comments is implemented, this
386        # test should be removed / improved to assert that new check.
387        self.flakes("""
388        from mod import F
389        x = 1
390        # type: F
391        """)
392
393    def test_typeIgnore(self):
394        self.flakes("""
395        a = 0  # type: ignore
396        b = 0  # type: ignore[excuse]
397        c = 0  # type: ignore=excuse
398        d = 0  # type: ignore [excuse]
399        e = 0  # type: ignore whatever
400        """)
401
402    def test_typeIgnoreBogus(self):
403        self.flakes("""
404        x = 1  # type: ignored
405        """, m.UndefinedName)
406
407    def test_typeIgnoreBogusUnicode(self):
408        error = (m.CommentAnnotationSyntaxError if version_info < (3,)
409                 else m.UndefinedName)
410        self.flakes("""
411        x = 2  # type: ignore\xc3
412        """, error)
413
414    @skipIf(version_info < (3,), 'new in Python 3')
415    def test_return_annotation_is_class_scope_variable(self):
416        self.flakes("""
417        from typing import TypeVar
418        class Test:
419            Y = TypeVar('Y')
420
421            def t(self, x: Y) -> Y:
422                return x
423        """)
424
425    @skipIf(version_info < (3,), 'new in Python 3')
426    def test_return_annotation_is_function_body_variable(self):
427        self.flakes("""
428        class Test:
429            def t(self) -> Y:
430                Y = 2
431                return Y
432        """, m.UndefinedName)
433
434    @skipIf(version_info < (3, 8), 'new in Python 3.8')
435    def test_positional_only_argument_annotations(self):
436        self.flakes("""
437        from x import C
438
439        def f(c: C, /): ...
440        """)
441
442    @skipIf(version_info < (3,), 'new in Python 3')
443    def test_partially_quoted_type_annotation(self):
444        self.flakes("""
445        from queue import Queue
446        from typing import Optional
447
448        def f() -> Optional['Queue[str]']:
449            return None
450        """)
451
452    def test_partially_quoted_type_assignment(self):
453        self.flakes("""
454        from queue import Queue
455        from typing import Optional
456
457        MaybeQueue = Optional['Queue[str]']
458        """)
459
460    def test_nested_partially_quoted_type_assignment(self):
461        self.flakes("""
462        from queue import Queue
463        from typing import Callable
464
465        Func = Callable[['Queue[str]'], None]
466        """)
467
468    def test_quoted_type_cast(self):
469        self.flakes("""
470        from typing import cast, Optional
471
472        maybe_int = cast('Optional[int]', 42)
473        """)
474
475    def test_type_cast_literal_str_to_str(self):
476        # Checks that our handling of quoted type annotations in the first
477        # argument to `cast` doesn't cause issues when (only) the _second_
478        # argument is a literal str which looks a bit like a type annoation.
479        self.flakes("""
480        from typing import cast
481
482        a_string = cast(str, 'Optional[int]')
483        """)
484
485    def test_quoted_type_cast_renamed_import(self):
486        self.flakes("""
487        from typing import cast as tsac, Optional as Maybe
488
489        maybe_int = tsac('Maybe[int]', 42)
490        """)
491
492    @skipIf(version_info < (3,), 'new in Python 3')
493    def test_literal_type_typing(self):
494        self.flakes("""
495        from typing import Literal
496
497        def f(x: Literal['some string']) -> None:
498            return None
499        """)
500
501    @skipIf(version_info < (3,), 'new in Python 3')
502    def test_literal_type_typing_extensions(self):
503        self.flakes("""
504        from typing_extensions import Literal
505
506        def f(x: Literal['some string']) -> None:
507            return None
508        """)
509
510    @skipIf(version_info < (3,), 'new in Python 3')
511    def test_literal_type_some_other_module(self):
512        """err on the side of false-negatives for types named Literal"""
513        self.flakes("""
514        from my_module import compat
515        from my_module.compat import Literal
516
517        def f(x: compat.Literal['some string']) -> None:
518            return None
519        def g(x: Literal['some string']) -> None:
520            return None
521        """)
522
523    @skipIf(version_info < (3,), 'new in Python 3')
524    def test_literal_union_type_typing(self):
525        self.flakes("""
526        from typing import Literal
527
528        def f(x: Literal['some string', 'foo bar']) -> None:
529            return None
530        """)
531
532    @skipIf(version_info < (3,), 'new in Python 3')
533    def test_deferred_twice_annotation(self):
534        self.flakes("""
535            from queue import Queue
536            from typing import Optional
537
538
539            def f() -> "Optional['Queue[str]']":
540                return None
541        """)
542
543    @skipIf(version_info < (3, 7), 'new in Python 3.7')
544    def test_partial_string_annotations_with_future_annotations(self):
545        self.flakes("""
546            from __future__ import annotations
547
548            from queue import Queue
549            from typing import Optional
550
551
552            def f() -> Optional['Queue[str]']:
553                return None
554        """)
555