Skip to content

Commit 5f7e937

Browse files
authored
Emit Y020 for quoted annotations used in TypeVar constraints (#407)
1 parent 52bcf19 commit 5f7e937

3 files changed

Lines changed: 24 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Bugfixes:
3131
TypeVars/ParamSpecs/TypeAliases/TypedDicts/Protocols if the object in question had
3232
multiple definitions in the same file (e.g. across two branches of an `if
3333
sys.version_info >= (3, 10)` check). This bug has now been fixed.
34+
* Y020 was previously not emitted if quoted annotations were used in TypeVar
35+
constraints. This bug has now been fixed.
3436

3537
Other changes:
3638
* flake8-pyi no longer supports being run on Python 3.7.

pyi.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,15 +1137,15 @@ def visit_Call(self, node: ast.Call) -> None:
11371137
):
11381138
return self.error(node, Y056.format(method=f".{function.attr}()"))
11391139

1140-
# String literals can appear in positional arguments for
1141-
# TypeVar definitions.
1142-
with self.string_literals_allowed.enabled():
1143-
for arg in node.args:
1144-
self.visit(arg)
1145-
# But in keyword arguments they're most likely TypeVar bounds,
1140+
# String literals can appear as the first positional argument for
1141+
# TypeVar/ParamSpec/TypeVarTuple/NamedTuple/TypedDict/NewType definitions, etc.
1142+
if node.args:
1143+
with self.string_literals_allowed.enabled():
1144+
self.visit(node.args[0])
1145+
# But in other arguments they're most likely TypeVar bounds,
11461146
# which should not be quoted.
1147-
for kw in node.keywords:
1148-
self.visit(kw)
1147+
for arg in chain(node.args[1:], node.keywords):
1148+
self.visit(arg)
11491149

11501150
def visit_Constant(self, node: ast.Constant) -> None:
11511151
if isinstance(node.value, str) and not self.string_literals_allowed.active:

tests/quotes.pyi

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22
import typing
3-
from typing import Annotated, Literal, TypeAlias, TypeVar
3+
from typing import Annotated, Literal, NewType, TypeAlias, TypeVar
44

55
import typing_extensions
66

@@ -15,6 +15,16 @@ __slots__ = ('foo',) # Y052 Need type annotation for "__slots__"
1515
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
1616
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
1717
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
18+
_T2 = TypeVar("_T", bound=int)
19+
_S = TypeVar("_S")
20+
_U = TypeVar("_U", "int", "str") # Y020 Quoted annotations should never be used in stubs # Y020 Quoted annotations should never be used in stubs
21+
_U2 = TypeVar("_U", int, str)
22+
23+
# This is invalid, but type checkers will flag it, so we don't need to
24+
_V = TypeVar()
25+
26+
def make_sure_those_typevars_arent_flagged_as_unused(a: _T, b: _T2, c: _S, d: _U, e: _U2, f: _V) -> tuple[_T, _T2, _S, _U, _U2, _V]: ...
27+
1828
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
1929

2030
def i(x: Annotated[int, "lots", "of", "strings"], b: typing.Annotated[str, "more", "strings"]) -> None:
@@ -60,3 +70,6 @@ class DocstringAndPass:
6070
k = "" # Y052 Need type annotation for "k"
6171
el = r"" # Y052 Need type annotation for "el"
6272
m = u"" # Y052 Need type annotation for "m"
73+
74+
_N = NewType("_N", int)
75+
_NBad = NewType("_N", "int") # Y020 Quoted annotations should never be used in stubs

0 commit comments

Comments
 (0)