Skip to content

Commit

Permalink
Emit Y020 for quoted annotations used in TypeVar constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jun 28, 2023
1 parent 52bcf19 commit 81da5f1
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Bugfixes:
TypeVars/ParamSpecs/TypeAliases/TypedDicts/Protocols if the object in question had
multiple definitions in the same file (e.g. across two branches of an `if
sys.version_info >= (3, 10)` check). This bug has now been fixed.
* Y020 was previously not emitted if quoted annotations were used in TypeVar
constraints. This bug has now been fixed.

Other changes:
* flake8-pyi no longer supports being run on Python 3.7.
Expand Down
14 changes: 7 additions & 7 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,15 +1137,15 @@ def visit_Call(self, node: ast.Call) -> None:
):
return self.error(node, Y056.format(method=f".{function.attr}()"))

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

def visit_Constant(self, node: ast.Constant) -> None:
if isinstance(node.value, str) and not self.string_literals_allowed.active:
Expand Down
15 changes: 14 additions & 1 deletion tests/quotes.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
import typing
from typing import Annotated, Literal, TypeAlias, TypeVar
from typing import Annotated, Literal, NewType, TypeAlias, TypeVar

import typing_extensions

Expand All @@ -15,6 +15,16 @@ __slots__ = ('foo',) # Y052 Need type annotation for "__slots__"
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
_T2 = TypeVar("_T", bound=int)
_S = TypeVar("_S")
_U = TypeVar("_U", "int", "str") # Y020 Quoted annotations should never be used in stubs # Y020 Quoted annotations should never be used in stubs
_U2 = TypeVar("_U", int, str)

# This is invalid, but type checkers will flag it, so we don't need to
_V = TypeVar()

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]: ...

def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...

def i(x: Annotated[int, "lots", "of", "strings"], b: typing.Annotated[str, "more", "strings"]) -> None:
Expand Down Expand Up @@ -60,3 +70,6 @@ class DocstringAndPass:
k = "" # Y052 Need type annotation for "k"
el = r"" # Y052 Need type annotation for "el"
m = u"" # Y052 Need type annotation for "m"

_N = NewType("_N", int)
_NBad = NewType("_N", "int") # Y020 Quoted annotations should never be used in stubs

0 comments on commit 81da5f1

Please sign in to comment.