Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 62 additions & 51 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,77 +1521,88 @@ def CALL(self, node):
):
self._handle_string_dot_format(node)

omit = []
annotated = []
not_annotated = []

if (
_is_typing(node.func, 'cast', self.scopeStack) and
len(node.args) >= 1
):
def _annotation(n: ast.AST) -> None:
with self._enter_annotation():
self.handleNode(node.args[0], node)
self.handleNode(n, node)

def _annotations(nodes: list[ast.AST]) -> None:
for n in nodes:
_annotation(n)

def _non_annotation(n: ast.AST) -> None:
with self._enter_annotation(AnnotationState.NONE):
self.handleNode(n, node)

def _non_annotations(nodes: list[ast.AST]) -> None:
for n in nodes:
_non_annotation(n)

if _is_typing(node.func, 'cast', self.scopeStack):
_non_annotation(node.func)

# cast("tp", val)
_annotations(node.args[:1])
_non_annotations(node.args[1:])

# cast(typ="tp", val=val)
for kwd in node.keywords:
if kwd.arg == 'typ':
_annotation(kwd)
else:
_non_annotation(kwd)

elif _is_typing(node.func, 'TypeVar', self.scopeStack):
_non_annotation(node.func)
_non_annotations(node.args[:1])

# TypeVar("T", "int", "str")
omit += ["args"]
annotated += [arg for arg in node.args[1:]]
_annotations(node.args[1:])

# TypeVar("T", bound="str")
omit += ["keywords"]
annotated += [k.value for k in node.keywords if k.arg == "bound"]
not_annotated += [
(k, ["value"] if k.arg == "bound" else None)
for k in node.keywords
]
for kwd in node.keywords:
if kwd.arg in ('bound', 'default'):
_annotation(kwd)
else:
_non_annotation(kwd)

elif _is_typing(node.func, "TypedDict", self.scopeStack):
_non_annotation(node.func)
_non_annotations(node.args[:1])

# TypedDict("a", {"a": int})
if len(node.args) > 1 and isinstance(node.args[1], ast.Dict):
omit += ["args"]
annotated += node.args[1].values
not_annotated += [
(arg, ["values"] if i == 1 else None)
for i, arg in enumerate(node.args)
]
_non_annotations(node.args[1].keys)
_annotations(node.args[1].values)
_non_annotations(node.args[2:])

# TypedDict("a", a=int)
omit += ["keywords"]
annotated += [k.value for k in node.keywords]
not_annotated += [(k, ["value"]) for k in node.keywords]
if sys.version_info >= (3, 13):
_non_annotations(node.keywords)
else:
_annotations(node.keywords)

elif _is_typing(node.func, "NamedTuple", self.scopeStack):
_non_annotation(node.func)
_non_annotations(node.args[:1])

# NamedTuple("a", [("a", int)])
if (
len(node.args) > 1 and
isinstance(node.args[1], (ast.Tuple, ast.List)) and
all(isinstance(x, (ast.Tuple, ast.List)) and
len(x.elts) == 2 for x in node.args[1].elts)
len(node.args) > 1 and
isinstance(node.args[1], (ast.Tuple, ast.List))
):
omit += ["args"]
annotated += [elt.elts[1] for elt in node.args[1].elts]
not_annotated += [(elt.elts[0], None) for elt in node.args[1].elts]
not_annotated += [
(arg, ["elts"] if i == 1 else None)
for i, arg in enumerate(node.args)
]
not_annotated += [(elt, "elts") for elt in node.args[1].elts]
for elt in node.args[1].elts:
if isinstance(elt, (ast.Tuple, ast.List)):
_non_annotations(elt.elts[:1])
_annotations(elt.elts[1:])
else:
_non_annotation(elt)
_non_annotations(node.args[2:])

# NamedTuple("a", a=int)
omit += ["keywords"]
annotated += [k.value for k in node.keywords]
not_annotated += [(k, ["value"]) for k in node.keywords]

if omit:
with self._enter_annotation(AnnotationState.NONE):
for na_node, na_omit in not_annotated:
self.handleChildren(na_node, omit=na_omit)
self.handleChildren(node, omit=omit)

with self._enter_annotation():
for annotated_node in annotated:
self.handleNode(annotated_node, node)
if sys.version_info >= (3, 15):
_non_annotations(node.keywords)
else:
_annotations(node.keywords)
else:
self.handleChildren(node)

Expand Down
57 changes: 54 additions & 3 deletions pyflakes/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,12 @@ def test_quoted_type_cast(self):
maybe_int = cast('Optional[int]', 42)
""")

def test_cast_named_argument(self):
self.flakes('''
from typing import cast, Optional
cast(typ='Optional[int]', val=42)
''')

def test_type_cast_literal_str_to_str(self):
# Checks that our handling of quoted type annotations in the first
# argument to `cast` doesn't cause issues when (only) the _second_
Expand All @@ -515,6 +521,12 @@ def test_quoted_type_cast_renamed_import(self):
maybe_int = tsac('Maybe[int]', 42)
""")

def test_cast_only_one_undefined(self):
self.flakes('''
from typing import cast
cast(undefined, 0)
''', m.UndefinedName)

def test_quoted_TypeVar_constraints(self):
self.flakes("""
from typing import TypeVar, Optional
Expand All @@ -530,6 +542,19 @@ def test_quoted_TypeVar_bound(self):
S = TypeVar('S', int, bound='List[int]')
""")

def test_quoted_TypeVar_default(self):
self.flakes('''
from typing import TypeVar, Optional
T = TypeVar('T', default='Optional[int]')
''')

def test_typevar_undefined_arg0(self):
self.flakes("""
from typing import TypeVar

T = TypeVar(T, int)
""", m.UndefinedName)

def test_literal_type_typing(self):
self.flakes("""
from typing import Literal
Expand Down Expand Up @@ -691,13 +716,11 @@ def test_typednames_correct_forward_ref(self):
from typing import TypedDict, List, NamedTuple, TypeVar

List[TypedDict("x", {"x": "Y"})]
List[TypedDict("x", x="Y")]
List[NamedTuple("a", [("a", "Y")])]
List[NamedTuple("a", a="Y")]
List[TypedDict("x", {"x": List["a"]})]
List[TypeVar("A", bound="C")]
List[TypeVar("A", List["C"])]
""", *[m.UndefinedName]*7)
""", *[m.UndefinedName]*5)
self.flakes("""
from typing import NamedTuple, TypeVar, cast
from t import A, B, C, D, E
Expand All @@ -708,6 +731,34 @@ def test_typednames_correct_forward_ref(self):
cast(A["E"], [])
""")

def test_namedtuple_kwargs(self):
if version_info >= (3, 15):
self.flakes('''
from typing import NamedTuple
from foo import T
NamedTuple("U", x="T")
''', m.UnusedImport)
else:
self.flakes('''
from typing import NamedTuple
from foo import T
NamedTuple("U", x="T")
''')

def test_typeddict_kwargs(self):
if version_info >= (3, 13):
self.flakes('''
from typing import TypedDict
from foo import T
TypedDict("U", x="T")
''', m.UnusedImport)
else:
self.flakes('''
from typing import TypedDict
from foo import T
TypedDict("U", x="T")
''')

def test_namedtypes_classes(self):
self.flakes("""
from typing import TypedDict, NamedTuple
Expand Down
Loading