From e980359a6c861e20fc278d3280bc266e6e13b22b Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 15:27:23 +0100 Subject: [PATCH 1/7] stubgen: Fix call-based namedtuple omitted from class bases Fixes #9901 Fixes #13662 Fix inheriting from a call-based `collections.namedtuple` / `typing.NamedTuple` definition that was omitted from the generated stub. This automatically adds support for the call-based `NamedTuple` in general not only as a based class (Closes #13788).
An example before and after Input: ```python import collections import typing from collections import namedtuple from typing import NamedTuple CollectionsCall = namedtuple("CollectionsCall", ["x", "y"]) class CollectionsClass(namedtuple("CollectionsClass", ["x", "y"])): def f(self, a): pass class CollectionsDotClass(collections.namedtuple("CollectionsClass", ["x", "y"])): def f(self, a): pass TypingCall = NamedTuple("TypingCall", [("x", int | None), ("y", int)]) class TypingClass(NamedTuple): x: int | None y: str def f(self, a): pass class TypingClassWeird(NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])): z: float | None def f(self, a): pass class TypingDotClassWeird(typing.NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])): def f(self, a): pass ``` Output diff (before and after): ```diff diff --git a/before.pyi b/after.pyi index c88530e2c..95ef843b4 100644 --- a/before.pyi +++ b/after.pyi @@ -1,26 +1,29 @@ +import typing from _typeshed import Incomplete from typing_extensions import NamedTuple class CollectionsCall(NamedTuple): x: Incomplete y: Incomplete -class CollectionsClass: +class CollectionsClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])): def f(self, a) -> None: ... -class CollectionsDotClass: +class CollectionsDotClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])): def f(self, a) -> None: ... -TypingCall: Incomplete +class TypingCall(NamedTuple): + x: int | None + y: int class TypingClass(NamedTuple): x: int | None y: str def f(self, a) -> None: ... -class TypingClassWeird: +class TypingClassWeird(NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])): z: float | None def f(self, a) -> None: ... -class TypingDotClassWeird: +class TypingDotClassWeird(typing.NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])): def f(self, a) -> None: ... ```
--- mypy/stubgen.py | 101 ++++++++++++++++++++++++++++++------ test-data/unit/stubgen.test | 52 ++++++++++++++++++- 2 files changed, 136 insertions(+), 17 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 6cb4669887fe..50be0c753a7a 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -411,6 +411,11 @@ def visit_op_expr(self, o: OpExpr) -> str: return f"{o.left.accept(self)} {o.op} {o.right.accept(self)}" +class NamedTuplePrinter(AliasPrinter): + def visit_tuple_expr(self, node: TupleExpr) -> str: + return f"({', '.join(n.accept(self) for n in node.items)})" + + class ImportTracker: """Record necessary imports during stub generation.""" @@ -986,6 +991,35 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: elif isinstance(base, IndexExpr): p = AliasPrinter(self) base_types.append(base.accept(p)) + elif isinstance(base, CallExpr): + # namedtuple(typename, fields), NamedTuple(typename, fields) calls can + # be used as a base class. The first argument is a string literal that + # is usually the same as the class name. + # Note: + # A call-based named tuple as a base class cannot be safely converted to + # a class-based NamedTuple definition because class attributes defined + # in the body of the class inheriting from the named tuple call are not + # namedtuple fields at runtime. + if self.is_namedtuple(base): + nt_fields = self._get_namedtuple_fields(base) + typename = cast(StrExpr, base.args[0]).value + if nt_fields is not None: + # A valid namedtuple() call, use NamedTuple() instead with + # Incomplete as field types + fields_str = ", ".join(f"({f!r}, {t})" for f, t in nt_fields) + else: + # Invalid namedtuple() call, cannot determine fields + fields_str = "Incomplete" + base_types.append(f"NamedTuple({typename!r}, [{fields_str}])") + self.add_typing_import("NamedTuple") + elif self.is_typed_namedtuple(base): + p = NamedTuplePrinter(self) + base_types.append(base.accept(p)) + else: + # At this point, we don't know what the base class is, so we + # just use Incomplete as the base class. + base_types.append("Incomplete") + self.import_tracker.require_name("Incomplete") return base_types def visit_block(self, o: Block) -> None: @@ -998,8 +1032,11 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: foundl = [] for lvalue in o.lvalues: - if isinstance(lvalue, NameExpr) and self.is_namedtuple(o.rvalue): - assert isinstance(o.rvalue, CallExpr) + if ( + isinstance(lvalue, NameExpr) + and isinstance(o.rvalue, CallExpr) + and (self.is_namedtuple(o.rvalue) or self.is_typed_namedtuple(o.rvalue)) + ): self.process_namedtuple(lvalue, o.rvalue) continue if ( @@ -1038,35 +1075,69 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: if all(foundl): self._state = VAR - def is_namedtuple(self, expr: Expression) -> bool: - if not isinstance(expr, CallExpr): - return False + def is_namedtuple(self, expr: CallExpr) -> bool: callee = expr.callee return (isinstance(callee, NameExpr) and callee.name.endswith("namedtuple")) or ( isinstance(callee, MemberExpr) and callee.name == "namedtuple" ) + def is_typed_namedtuple(self, expr: CallExpr) -> bool: + callee = expr.callee + return (isinstance(callee, NameExpr) and callee.name.endswith("NamedTuple")) or ( + isinstance(callee, MemberExpr) and callee.name == "NamedTuple" + ) + + def _get_namedtuple_fields(self, call: CallExpr) -> list[tuple[str, str]] | None: + if self.is_namedtuple(call): + items: list[str] + if isinstance(call.args[1], StrExpr): + items = call.args[1].value.replace(",", " ").split() + elif isinstance(call.args[1], (ListExpr, TupleExpr)): + items = [] + for item in call.args[1].items: + if isinstance(item, StrExpr): + items.append(item.value) + else: + return None # Invalid namedtuple field type + else: + return None # Invalid namedtuple fields type + if items: + self.import_tracker.require_name("Incomplete") + return [(item, "Incomplete") for item in items] + elif self.is_typed_namedtuple(call): + fields: list[tuple[str, str]] = [] + if isinstance(call.args[1], (ListExpr, TupleExpr)): + b = AliasPrinter(self) + for item in call.args[1].items: + if isinstance(item, TupleExpr) and len(item.items) == 2: + field_name, field_type = item.items + if not isinstance(field_name, StrExpr): + return None # Invalid NamedTuple field name + fields.append((field_name.value, field_type.accept(b))) + else: + return None # Invalid NamedTuple field type + else: + return None # Invalid NamedTuple fields type + return fields + else: + return None # Not a named tuple call + def process_namedtuple(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if self._state != EMPTY: self.add("\n") - if isinstance(rvalue.args[1], StrExpr): - items = rvalue.args[1].value.replace(",", " ").split() - elif isinstance(rvalue.args[1], (ListExpr, TupleExpr)): - list_items = cast(List[StrExpr], rvalue.args[1].items) - items = [item.value for item in list_items] - else: + fields = self._get_namedtuple_fields(rvalue) + if fields is None: self.add(f"{self._indent}{lvalue.name}: Incomplete") self.import_tracker.require_name("Incomplete") return self.import_tracker.require_name("NamedTuple") self.add(f"{self._indent}class {lvalue.name}(NamedTuple):") - if len(items) == 0: + if len(fields) == 0: self.add(" ...\n") else: - self.import_tracker.require_name("Incomplete") self.add("\n") - for item in items: - self.add(f"{self._indent} {item}: Incomplete\n") + for f_name, f_type in fields: + self.add(f"{self._indent} {f_name}: {f_type}\n") self._state = CLASS def is_alias_expression(self, expr: Expression, top_level: bool = True) -> bool: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 8e4285b7de2e..7de0635028e9 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -585,8 +585,9 @@ class A: def _bar(cls) -> None: ... [case testNamedtuple] -import collections, x +import collections, typing, x X = collections.namedtuple('X', ['a', 'b']) +Y = typing.NamedTuple('Y', [('a', int), ('b', str)]) [out] from _typeshed import Incomplete from typing import NamedTuple @@ -595,14 +596,21 @@ class X(NamedTuple): a: Incomplete b: Incomplete +class Y(NamedTuple): + a: int + b: str + [case testEmptyNamedtuple] -import collections +import collections, typing X = collections.namedtuple('X', []) +Y = typing.NamedTuple('Y', []) [out] from typing import NamedTuple class X(NamedTuple): ... +class Y(NamedTuple): ... + [case testNamedtupleAltSyntax] from collections import namedtuple, xx X = namedtuple('X', 'a b') @@ -641,8 +649,10 @@ class X(NamedTuple): [case testNamedtupleWithUnderscore] from collections import namedtuple as _namedtuple +from typing import NamedTuple as _NamedTuple def f(): ... X = _namedtuple('X', 'a b') +Y = _NamedTuple('Y', [('a', int), ('b', str)]) def g(): ... [out] from _typeshed import Incomplete @@ -654,6 +664,10 @@ class X(NamedTuple): a: Incomplete b: Incomplete +class Y(NamedTuple): + a: int + b: str + def g() -> None: ... [case testNamedtupleBaseClass] @@ -672,10 +686,14 @@ class Y(_X): ... [case testNamedtupleAltSyntaxFieldsTuples] from collections import namedtuple, xx +from typing import NamedTuple X = namedtuple('X', ()) Y = namedtuple('Y', ('a',)) Z = namedtuple('Z', ('a', 'b', 'c', 'd', 'e')) xx +R = NamedTuple('R', ()) +S = NamedTuple('S', (('a', int),)) +T = NamedTuple('T', (('a', int), ('b', str))) [out] from _typeshed import Incomplete from typing import NamedTuple @@ -692,13 +710,43 @@ class Z(NamedTuple): d: Incomplete e: Incomplete +class R(NamedTuple): ... + +class S(NamedTuple): + a: int + +class T(NamedTuple): + a: int + b: str + [case testDynamicNamedTuple] from collections import namedtuple +from typing import NamedTuple N = namedtuple('N', ['x', 'y'] + ['z']) +M = NamedTuple('M', [('x', int), ('y', str)] + [('z', float)]) [out] from _typeshed import Incomplete N: Incomplete +M: Incomplete + +[case testNamedTupleInClassBases] +import collections, typing +from collections import namedtuple +from typing import NamedTuple +class X(namedtuple('X', ['a', 'b'])): ... +class Y(NamedTuple('Y', [('a', int), ('b', str)])): ... +class R(collections.namedtuple('R', ['a', 'b'])): ... +class S(typing.NamedTuple('S', [('a', int), ('b', str)])): ... +[out] +import typing +from _typeshed import Incomplete +from typing import NamedTuple + +class X(NamedTuple('X', [('a', Incomplete), ('b', Incomplete)])): ... +class Y(NamedTuple('Y', [('a', int), ('b', str)])): ... +class R(NamedTuple('R', [('a', Incomplete), ('b', Incomplete)])): ... +class S(typing.NamedTuple('S', [('a', int), ('b', str)])): ... [case testArbitraryBaseClass] import x From 593d66cea4363d4e36f79df134bfc4b414b96e68 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 22:07:58 +0100 Subject: [PATCH 2/7] Fix state --- mypy/stubgen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 50be0c753a7a..788291392e40 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1134,11 +1134,12 @@ def process_namedtuple(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self.add(f"{self._indent}class {lvalue.name}(NamedTuple):") if len(fields) == 0: self.add(" ...\n") + self._state = EMPTY_CLASS else: self.add("\n") for f_name, f_type in fields: self.add(f"{self._indent} {f_name}: {f_type}\n") - self._state = CLASS + self._state = CLASS def is_alias_expression(self, expr: Expression, top_level: bool = True) -> bool: """Return True for things that look like target for an alias. From 75781685f25c94956266ac40f30a5405c718899e Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 22:48:54 +0100 Subject: [PATCH 3/7] Improve named tuples detection --- mypy/stubgen.py | 19 +++++++++++++++---- test-data/unit/stubgen.test | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 788291392e40..b96b63c76d47 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -125,6 +125,7 @@ from mypy.traverser import all_yield_expressions, has_return_statement, has_yield_expression from mypy.types import ( OVERLOAD_NAMES, + TYPED_NAMEDTUPLE_NAMES, AnyType, CallableType, Instance, @@ -1077,14 +1078,24 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: def is_namedtuple(self, expr: CallExpr) -> bool: callee = expr.callee - return (isinstance(callee, NameExpr) and callee.name.endswith("namedtuple")) or ( - isinstance(callee, MemberExpr) and callee.name == "namedtuple" + return ( + isinstance(callee, NameExpr) + and (self.refers_to_fullname(callee.name, "collections.namedtuple")) + ) or ( + isinstance(callee, MemberExpr) + and isinstance(callee.expr, NameExpr) + and f"{callee.expr.name}.{callee.name}" in "collections.namedtuple" ) def is_typed_namedtuple(self, expr: CallExpr) -> bool: callee = expr.callee - return (isinstance(callee, NameExpr) and callee.name.endswith("NamedTuple")) or ( - isinstance(callee, MemberExpr) and callee.name == "NamedTuple" + return ( + isinstance(callee, NameExpr) + and self.refers_to_fullname(callee.name, TYPED_NAMEDTUPLE_NAMES) + ) or ( + isinstance(callee, MemberExpr) + and isinstance(callee.expr, NameExpr) + and f"{callee.expr.name}.{callee.name}" in TYPED_NAMEDTUPLE_NAMES ) def _get_namedtuple_fields(self, call: CallExpr) -> list[tuple[str, str]] | None: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 7de0635028e9..a801f1623546 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -748,6 +748,23 @@ class Y(NamedTuple('Y', [('a', int), ('b', str)])): ... class R(NamedTuple('R', [('a', Incomplete), ('b', Incomplete)])): ... class S(typing.NamedTuple('S', [('a', int), ('b', str)])): ... +[case testNotNamedTuple] +from not_collections import namedtuple +from not_typing import NamedTuple +from collections import notnamedtuple +from typing import NotNamedTuple +X = namedtuple('X', ['a', 'b']) +Y = notnamedtuple('Y', ['a', 'b']) +Z = NamedTuple('Z', [('a', int), ('b', str)]) +W = NotNamedTuple('W', [('a', int), ('b', str)]) +[out] +from _typeshed import Incomplete + +X: Incomplete +Y: Incomplete +Z: Incomplete +W: Incomplete + [case testArbitraryBaseClass] import x class D(x.C): ... From 19a984f07c601e8a80f521570770f90422c8102c Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 10 Mar 2023 22:03:46 +0100 Subject: [PATCH 4/7] Code review --- mypy/stubgen.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index b96b63c76d47..b07c0aa8dccc 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -48,7 +48,7 @@ import sys import traceback from collections import defaultdict -from typing import Iterable, List, Mapping, cast +from typing import Iterable, List, Mapping from typing_extensions import Final import mypy.build @@ -996,6 +996,7 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: # namedtuple(typename, fields), NamedTuple(typename, fields) calls can # be used as a base class. The first argument is a string literal that # is usually the same as the class name. + # # Note: # A call-based named tuple as a base class cannot be safely converted to # a class-based NamedTuple definition because class attributes defined @@ -1003,7 +1004,8 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: # namedtuple fields at runtime. if self.is_namedtuple(base): nt_fields = self._get_namedtuple_fields(base) - typename = cast(StrExpr, base.args[0]).value + assert isinstance(base.args[0], StrExpr) + typename = base.args[0].value if nt_fields is not None: # A valid namedtuple() call, use NamedTuple() instead with # Incomplete as field types @@ -1084,7 +1086,7 @@ def is_namedtuple(self, expr: CallExpr) -> bool: ) or ( isinstance(callee, MemberExpr) and isinstance(callee.expr, NameExpr) - and f"{callee.expr.name}.{callee.name}" in "collections.namedtuple" + and f"{callee.expr.name}.{callee.name}" == "collections.namedtuple" ) def is_typed_namedtuple(self, expr: CallExpr) -> bool: @@ -1116,8 +1118,9 @@ def _get_namedtuple_fields(self, call: CallExpr) -> list[tuple[str, str]] | None self.import_tracker.require_name("Incomplete") return [(item, "Incomplete") for item in items] elif self.is_typed_namedtuple(call): - fields: list[tuple[str, str]] = [] + if isinstance(call.args[1], (ListExpr, TupleExpr)): + fields: list[tuple[str, str]] = [] b = AliasPrinter(self) for item in call.args[1].items: if isinstance(item, TupleExpr) and len(item.items) == 2: @@ -1127,9 +1130,9 @@ def _get_namedtuple_fields(self, call: CallExpr) -> list[tuple[str, str]] | None fields.append((field_name.value, field_type.accept(b))) else: return None # Invalid NamedTuple field type + return fields else: return None # Invalid NamedTuple fields type - return fields else: return None # Not a named tuple call From 3fc46a70915f63f557d73f2176786d877588b07c Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 10 Mar 2023 22:07:06 +0100 Subject: [PATCH 5/7] Delete NamedTuplePrinter --- mypy/stubgen.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index b07c0aa8dccc..43c7d2c1ca9c 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -397,10 +397,12 @@ def visit_str_expr(self, node: StrExpr) -> str: def visit_index_expr(self, node: IndexExpr) -> str: base = node.base.accept(self) index = node.index.accept(self) + if len(index) > 2 and index.startswith("(") and index.endswith(")"): + index = index[1:-1] return f"{base}[{index}]" def visit_tuple_expr(self, node: TupleExpr) -> str: - return ", ".join(n.accept(self) for n in node.items) + return f"({', '.join(n.accept(self) for n in node.items)})" def visit_list_expr(self, node: ListExpr) -> str: return f"[{', '.join(n.accept(self) for n in node.items)}]" @@ -412,11 +414,6 @@ def visit_op_expr(self, o: OpExpr) -> str: return f"{o.left.accept(self)} {o.op} {o.right.accept(self)}" -class NamedTuplePrinter(AliasPrinter): - def visit_tuple_expr(self, node: TupleExpr) -> str: - return f"({', '.join(n.accept(self) for n in node.items)})" - - class ImportTracker: """Record necessary imports during stub generation.""" @@ -1016,7 +1013,7 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: base_types.append(f"NamedTuple({typename!r}, [{fields_str}])") self.add_typing_import("NamedTuple") elif self.is_typed_namedtuple(base): - p = NamedTuplePrinter(self) + p = AliasPrinter(self) base_types.append(base.accept(p)) else: # At this point, we don't know what the base class is, so we From c6a606c7049ef099bc170a3d99411c8d56b34ea8 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 10 Mar 2023 22:42:20 +0100 Subject: [PATCH 6/7] Early return --- mypy/stubgen.py | 51 +++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 43c7d2c1ca9c..c1fe47f17228 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1099,37 +1099,34 @@ def is_typed_namedtuple(self, expr: CallExpr) -> bool: def _get_namedtuple_fields(self, call: CallExpr) -> list[tuple[str, str]] | None: if self.is_namedtuple(call): - items: list[str] - if isinstance(call.args[1], StrExpr): - items = call.args[1].value.replace(",", " ").split() - elif isinstance(call.args[1], (ListExpr, TupleExpr)): - items = [] - for item in call.args[1].items: - if isinstance(item, StrExpr): - items.append(item.value) - else: - return None # Invalid namedtuple field type + fields_arg = call.args[1] + if isinstance(fields_arg, StrExpr): + field_names = fields_arg.value.replace(",", " ").split() + elif isinstance(fields_arg, (ListExpr, TupleExpr)): + field_names = [] + for field in fields_arg.items: + if not isinstance(field, StrExpr): + return None + field_names.append(field.value) else: return None # Invalid namedtuple fields type - if items: + if field_names: self.import_tracker.require_name("Incomplete") - return [(item, "Incomplete") for item in items] + return [(field_name, "Incomplete") for field_name in field_names] elif self.is_typed_namedtuple(call): - - if isinstance(call.args[1], (ListExpr, TupleExpr)): - fields: list[tuple[str, str]] = [] - b = AliasPrinter(self) - for item in call.args[1].items: - if isinstance(item, TupleExpr) and len(item.items) == 2: - field_name, field_type = item.items - if not isinstance(field_name, StrExpr): - return None # Invalid NamedTuple field name - fields.append((field_name.value, field_type.accept(b))) - else: - return None # Invalid NamedTuple field type - return fields - else: - return None # Invalid NamedTuple fields type + fields_arg = call.args[1] + if not isinstance(fields_arg, (ListExpr, TupleExpr)): + return None + fields: list[tuple[str, str]] = [] + b = AliasPrinter(self) + for field in fields_arg.items: + if not (isinstance(field, TupleExpr) and len(field.items) == 2): + return None + field_name, field_type = field.items + if not isinstance(field_name, StrExpr): + return None + fields.append((field_name.value, field_type.accept(b))) + return fields else: return None # Not a named tuple call From f2ba8ed1e4af66e01b83b96fb0d5f4295f7ab4d6 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 6 May 2023 16:45:48 +0200 Subject: [PATCH 7/7] Fix and test invalid namedtuple case --- mypy/stubgen.py | 6 +++--- test-data/unit/stubgen.test | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 17d92dbd4162..071a238b5714 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1030,11 +1030,11 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: # A valid namedtuple() call, use NamedTuple() instead with # Incomplete as field types fields_str = ", ".join(f"({f!r}, {t})" for f, t in nt_fields) + base_types.append(f"NamedTuple({typename!r}, [{fields_str}])") + self.add_typing_import("NamedTuple") else: # Invalid namedtuple() call, cannot determine fields - fields_str = "Incomplete" - base_types.append(f"NamedTuple({typename!r}, [{fields_str}])") - self.add_typing_import("NamedTuple") + base_types.append("Incomplete") elif self.is_typed_namedtuple(base): p = AliasPrinter(self) base_types.append(base.accept(p)) diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index a0c289222a23..eba838f32cd5 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -780,11 +780,13 @@ from collections import namedtuple from typing import NamedTuple N = namedtuple('N', ['x', 'y'] + ['z']) M = NamedTuple('M', [('x', int), ('y', str)] + [('z', float)]) +class X(namedtuple('X', ['a', 'b'] + ['c'])): ... [out] from _typeshed import Incomplete N: Incomplete M: Incomplete +class X(Incomplete): ... [case testNamedTupleInClassBases] import collections, typing