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