From 56666e6fe96e3d92b24f9393be873285b96ca1eb Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 21:36:17 +0100 Subject: [PATCH 01/12] stubgen: Support TypedDict alternative syntax Fixes #14681 --- mypy/stubgen.py | 47 +++++++++++++++++++++++++++++- test-data/unit/stubgen.test | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 6cb4669887fe..0c407760da96 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -80,6 +80,7 @@ ClassDef, ComparisonExpr, Decorator, + DictExpr, EllipsisExpr, Expression, FloatExpr, @@ -125,6 +126,7 @@ from mypy.traverser import all_yield_expressions, has_return_statement, has_yield_expression from mypy.types import ( OVERLOAD_NAMES, + TPDICT_NAMES, AnyType, CallableType, Instance, @@ -398,6 +400,9 @@ def visit_index_expr(self, node: IndexExpr) -> str: index = node.index.accept(self) return f"{base}[{index}]" + def visit_dict_expr(self, o: DictExpr) -> str: + return "{%s}" % ", ".join(f"{k.accept(self)}: {v.accept(self)}" for k, v in o.items) + def visit_tuple_expr(self, node: TupleExpr) -> str: return ", ".join(n.accept(self) for n in node.items) @@ -622,7 +627,7 @@ def __init__( # Disable implicit exports of package-internal imports? self.export_less = export_less # Add imports that could be implicitly generated - self.import_tracker.add_import_from("typing", [("NamedTuple", None)]) + self.import_tracker.add_import_from("typing", [("NamedTuple", None), ("TypedDict", None)]) # Names in __all__ are required for name in _all_ or (): if name not in IGNORED_DUNDERS: @@ -1002,6 +1007,10 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert isinstance(o.rvalue, CallExpr) self.process_namedtuple(lvalue, o.rvalue) continue + if isinstance(lvalue, NameExpr) and self.is_typeddict(o.rvalue): + assert isinstance(o.rvalue, CallExpr) + self.process_typeddict(lvalue, o.rvalue) + continue if ( isinstance(lvalue, NameExpr) and not self.is_private_name(lvalue.name) @@ -1069,6 +1078,42 @@ def process_namedtuple(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self.add(f"{self._indent} {item}: Incomplete\n") self._state = CLASS + def is_typeddict(self, expr: Expression) -> bool: + if not isinstance(expr, CallExpr): + return False + callee = expr.callee + return isinstance(callee, (NameExpr, MemberExpr)) and any( + self.refers_to_fullname(callee.name, tpdict_name) for tpdict_name in TPDICT_NAMES + ) + + def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: + if self._state != EMPTY: + self.add("\n") + if isinstance(rvalue.args[1], DictExpr): + items_list = cast(list[tuple[StrExpr, Expression]], rvalue.args[1].items) + items = [(item[0].value, item[1]) for item in items_list] + else: + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return + self.import_tracker.require_name("TypedDict") + p = AliasPrinter(self) + if not all(key.isidentifier() for key, _ in items): + # Keep the call syntax if there are non-identifier keys. + self.add(f"{self._indent}{lvalue.name} = {rvalue.accept(p)}\n") + else: + bases = "TypedDict" + if len(rvalue.args) > 2: + bases += f", total={rvalue.args[2].accept(p)}" + self.add(f"{self._indent}class {lvalue.name}({bases}):") + if len(items) == 0: + self.add(" ...\n") + else: + self.add("\n") + for key, key_type in items: + self.add(f"{self._indent} {key}: {key_type.accept(p)}\n") + 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. diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 8e4285b7de2e..11aef4113b4d 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2793,3 +2793,61 @@ def f(x: str | None) -> None: ... a: str | int def f(x: str | None) -> None: ... + +[case testTypeddict] +import typing, x +X = typing.TypedDict('X', {'a': int, 'b': str}) +Y = typing.TypedDict('X', {'a': int, 'b': str}, total=False) +[out] +from typing import TypedDict + +class X(TypedDict): + a: int + b: str + +class Y(TypedDict, total=False): + a: int + b: str + +[case testTypeddictWithNonIdentifierKeys] +from typing import TypedDict +X = TypedDict('X', {'a-b': int, 'c': str}) +Y = TypedDict('X', {'a-b': int, 'c': str}, total=False) +[out] +from typing import TypedDict + +X = TypedDict('X', {'a-b': int, 'c': str}) + +Y = TypedDict('X', {'a-b': int, 'c': str}, total=False) + +[case testEmptyTypeddict] +import typing +X = typing.TypedDict('X', {}) +[out] +from typing import TypedDict + +class X(TypedDict): ... + +[case testTypeddictWithUnderscore] +from typing import TypedDict as _TypedDict +def f(): ... +X = _TypedDict('X', {'a': int, 'b': str}) +def g(): ... +[out] +from typing import TypedDict + +def f() -> None: ... + +class X(TypedDict): + a: int + b: str + +def g() -> None: ... + +[case testTypeddictUnknownImport] +from x import TypedDict +X = TypedDict('X', {'a': int, 'b': str}) +[out] +from _typeshed import Incomplete + +X: Incomplete From 560e744091d4bbb10f0a685f663b507f87cb769f Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 21:50:37 +0100 Subject: [PATCH 02/12] Fix status --- mypy/stubgen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 0c407760da96..0b9c522ff6a8 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1101,6 +1101,7 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if not all(key.isidentifier() for key, _ in items): # Keep the call syntax if there are non-identifier keys. self.add(f"{self._indent}{lvalue.name} = {rvalue.accept(p)}\n") + self._state = VAR else: bases = "TypedDict" if len(rvalue.args) > 2: @@ -1108,11 +1109,12 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self.add(f"{self._indent}class {lvalue.name}({bases}):") if len(items) == 0: self.add(" ...\n") + self._state = EMPTY_CLASS else: self.add("\n") for key, key_type in items: self.add(f"{self._indent} {key}: {key_type.accept(p)}\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 3f2c434098c6f77dff6a113fcf3e2829243e04b2 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 21:59:50 +0100 Subject: [PATCH 03/12] Fix mypy error --- mypy/stubgen.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 0b9c522ff6a8..6f2b8e8880ff 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -400,15 +400,18 @@ def visit_index_expr(self, node: IndexExpr) -> str: index = node.index.accept(self) return f"{base}[{index}]" - def visit_dict_expr(self, o: DictExpr) -> str: - return "{%s}" % ", ".join(f"{k.accept(self)}: {v.accept(self)}" for k, v in o.items) - def visit_tuple_expr(self, node: TupleExpr) -> str: return ", ".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)}]" + def visit_dict_expr(self, o: DictExpr) -> str: + # This is currently only used for TypedDict where all keys are strings. + return "{%s}" % ", ".join( + f"{cast(StrExpr, k).accept(self)}: {v.accept(self)}" for k, v in o.items + ) + def visit_ellipsis(self, node: EllipsisExpr) -> str: return "..." From 0e617b6a2ccd04c28c01a2abb2d030644c5153d4 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 22:53:10 +0100 Subject: [PATCH 04/12] Fix mypy self check for python 3.8 --- mypy/stubgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 6f2b8e8880ff..80166c5acf2e 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, Tuple, cast from typing_extensions import Final import mypy.build @@ -1093,7 +1093,7 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if self._state != EMPTY: self.add("\n") if isinstance(rvalue.args[1], DictExpr): - items_list = cast(list[tuple[StrExpr, Expression]], rvalue.args[1].items) + items_list = cast(List[Tuple[StrExpr, Expression]], rvalue.args[1].items) items = [(item[0].value, item[1]) for item in items_list] else: self.add(f"{self._indent}{lvalue.name}: Incomplete") From edeba78dfbdc8cb252dc8a945034d94a50cff5e6 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 11 Feb 2023 23:02:14 +0100 Subject: [PATCH 05/12] Improve typed dict detection logic --- mypy/stubgen.py | 8 ++++++-- test-data/unit/stubgen.test | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 80166c5acf2e..340eaadf75cb 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1085,8 +1085,12 @@ def is_typeddict(self, expr: Expression) -> bool: if not isinstance(expr, CallExpr): return False callee = expr.callee - return isinstance(callee, (NameExpr, MemberExpr)) and any( - self.refers_to_fullname(callee.name, tpdict_name) for tpdict_name in TPDICT_NAMES + return ( + isinstance(callee, NameExpr) and self.refers_to_fullname(callee.name, TPDICT_NAMES) + ) or ( + isinstance(callee, MemberExpr) + and isinstance(callee.expr, NameExpr) + and f"{callee.expr.name}.{callee.name}" in TPDICT_NAMES ) def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 11aef4113b4d..5c1f25415962 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2844,10 +2844,13 @@ class X(TypedDict): def g() -> None: ... -[case testTypeddictUnknownImport] +[case testNotTypeddict] from x import TypedDict +import y X = TypedDict('X', {'a': int, 'b': str}) +Y = y.TypedDict('Y', {'a': int, 'b': str}) [out] from _typeshed import Incomplete X: Incomplete +Y: Incomplete From 5a85e1d379cdb3df5d793421630bb1bb4a33ae98 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Mar 2023 20:03:49 +0200 Subject: [PATCH 06/12] Fail gracefully with invalid attribute types --- mypy/stubgen.py | 11 ++++++++--- test-data/unit/stubgen.test | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 55c3479f6522..f4b7e626868a 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, Tuple, cast +from typing import Iterable, List, Mapping, cast from typing_extensions import Final import mypy.build @@ -1099,8 +1099,13 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if self._state != EMPTY: self.add("\n") if isinstance(rvalue.args[1], DictExpr): - items_list = cast(List[Tuple[StrExpr, Expression]], rvalue.args[1].items) - items = [(item[0].value, item[1]) for item in items_list] + items: list[tuple[str, Expression]] = [] + for attr_name, attr_type in rvalue.args[1].items: + if not isinstance(attr_name, StrExpr): + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return + items.append((attr_name.value, attr_type)) else: self.add(f"{self._indent}{lvalue.name}: Incomplete") self.import_tracker.require_name("Incomplete") diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 5c1f25415962..50321092f5f6 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2854,3 +2854,11 @@ from _typeshed import Incomplete X: Incomplete Y: Incomplete + +[case testTypeddictWithWrongAttributesType] +from typing import TypedDict +T = TypeDict("T", {"a": int, **{"b": str, "c": bytes}}) +[out] +from _typeshed import Incomplete + +T: Incomplete From 1bd4c130b17af3da20e024542ca14ee075f2d87a Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Mar 2023 22:00:17 +0200 Subject: [PATCH 07/12] Preserve call syntax for keyword attributes --- mypy/stubgen.py | 5 +++-- test-data/unit/stubgen.test | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index f4b7e626868a..c01755d9c400 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -43,6 +43,7 @@ import argparse import glob +import keyword import os import os.path import sys @@ -1112,8 +1113,8 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: return self.import_tracker.require_name("TypedDict") p = AliasPrinter(self) - if not all(key.isidentifier() for key, _ in items): - # Keep the call syntax if there are non-identifier keys. + if any(not key.isidentifier() or keyword.iskeyword(key) for key, _ in items): + # Keep the call syntax if there are non-identifier or keyword keys. self.add(f"{self._indent}{lvalue.name} = {rvalue.accept(p)}\n") self._state = VAR else: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 50321092f5f6..38f0984876ff 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2809,10 +2809,11 @@ class Y(TypedDict, total=False): a: int b: str -[case testTypeddictWithNonIdentifierKeys] +[case testTypeddictWithNonIdentifierOrKeywordKeys] from typing import TypedDict X = TypedDict('X', {'a-b': int, 'c': str}) Y = TypedDict('X', {'a-b': int, 'c': str}, total=False) +Z = TypedDict('X', {'a': int, 'in': str}) [out] from typing import TypedDict @@ -2820,6 +2821,8 @@ X = TypedDict('X', {'a-b': int, 'c': str}) Y = TypedDict('X', {'a-b': int, 'c': str}, total=False) +Z = TypedDict('X', {'a': int, 'in': str}) + [case testEmptyTypeddict] import typing X = typing.TypedDict('X', {}) From fe32c21d869ac38631231eadf4c1444e308bae4c Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Mar 2023 22:07:15 +0200 Subject: [PATCH 08/12] Use assert instead of cast --- mypy/stubgen.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index c01755d9c400..d8826b27b48b 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -49,7 +49,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 @@ -409,10 +409,12 @@ def visit_list_expr(self, node: ListExpr) -> str: return f"[{', '.join(n.accept(self) for n in node.items)}]" def visit_dict_expr(self, o: DictExpr) -> str: - # This is currently only used for TypedDict where all keys are strings. - return "{%s}" % ", ".join( - f"{cast(StrExpr, k).accept(self)}: {v.accept(self)}" for k, v in o.items - ) + dict_items = [] + for key, value in o.items: + # This is currently only used for TypedDict where all keys are strings. + assert isinstance(key, StrExpr) + dict_items.append(f"{key.accept(self)}: {value.accept(self)}") + return f"{{{', '.join(dict_items)}}}" def visit_ellipsis(self, node: EllipsisExpr) -> str: return "..." From a6fbd4a9dd647d756193059c6d0d58e3fe1f0c19 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Mar 2023 23:32:11 +0200 Subject: [PATCH 09/12] Import from typing_extensions to support python 3.7 --- mypy/stubgen.py | 3 ++- test-data/unit/stubgen.test | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index d8826b27b48b..e693bea39d1f 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -634,7 +634,7 @@ def __init__( # Disable implicit exports of package-internal imports? self.export_less = export_less # Add imports that could be implicitly generated - self.import_tracker.add_import_from("typing", [("NamedTuple", None), ("TypedDict", None)]) + self.import_tracker.add_import_from("typing", [("NamedTuple", None)]) # Names in __all__ are required for name in _all_ or (): if name not in IGNORED_DUNDERS: @@ -652,6 +652,7 @@ def visit_mypy_file(self, o: MypyFile) -> None: "_typeshed": ["Incomplete"], "typing": ["Any", "TypeVar"], "collections.abc": ["Generator"], + "typing_extensions": ["TypedDict"], } for pkg, imports in known_imports.items(): for t in imports: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 38f0984876ff..3f416b4f7745 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2799,7 +2799,7 @@ import typing, x X = typing.TypedDict('X', {'a': int, 'b': str}) Y = typing.TypedDict('X', {'a': int, 'b': str}, total=False) [out] -from typing import TypedDict +from typing_extensions import TypedDict class X(TypedDict): a: int @@ -2827,7 +2827,7 @@ Z = TypedDict('X', {'a': int, 'in': str}) import typing X = typing.TypedDict('X', {}) [out] -from typing import TypedDict +from typing_extensions import TypedDict class X(TypedDict): ... @@ -2837,7 +2837,7 @@ def f(): ... X = _TypedDict('X', {'a': int, 'b': str}) def g(): ... [out] -from typing import TypedDict +from typing_extensions import TypedDict def f() -> None: ... From d9cc7f54da4f57ee0b9eedab56d20a67161c9691 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Mar 2023 23:39:52 +0200 Subject: [PATCH 10/12] Code Review: typed dict call expression --- mypy/stubgen.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index e693bea39d1f..fb4c29254210 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1015,8 +1015,11 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: assert isinstance(o.rvalue, CallExpr) self.process_namedtuple(lvalue, o.rvalue) continue - if isinstance(lvalue, NameExpr) and self.is_typeddict(o.rvalue): - assert isinstance(o.rvalue, CallExpr) + if ( + isinstance(lvalue, NameExpr) + and isinstance(o.rvalue, CallExpr) + and self.is_typeddict(o.rvalue) + ): self.process_typeddict(lvalue, o.rvalue) continue if ( @@ -1087,9 +1090,7 @@ def process_namedtuple(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self.add(f"{self._indent} {item}: Incomplete\n") self._state = CLASS - def is_typeddict(self, expr: Expression) -> bool: - if not isinstance(expr, CallExpr): - return False + def is_typeddict(self, expr: CallExpr) -> bool: callee = expr.callee return ( isinstance(callee, NameExpr) and self.refers_to_fullname(callee.name, TPDICT_NAMES) From b42f9b60815977225d59ec56bf6c83e1bde81a32 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 31 Mar 2023 22:46:12 +0200 Subject: [PATCH 11/12] Support keyword-syntax and add TODO about generic --- mypy/stubgen.py | 30 +++++++++++++++++++++++------- test-data/unit/stubgen.test | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index fb4c29254210..a113683ea6a8 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1103,18 +1103,33 @@ def is_typeddict(self, expr: CallExpr) -> bool: def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if self._state != EMPTY: self.add("\n") - if isinstance(rvalue.args[1], DictExpr): - items: list[tuple[str, Expression]] = [] + + if not isinstance(rvalue.args[0], StrExpr): + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return + + items: list[tuple[str, Expression]] = [] + total: Expression | None = None + if len(rvalue.args) > 1 and isinstance(rvalue.args[1], DictExpr): for attr_name, attr_type in rvalue.args[1].items: if not isinstance(attr_name, StrExpr): self.add(f"{self._indent}{lvalue.name}: Incomplete") self.import_tracker.require_name("Incomplete") return items.append((attr_name.value, attr_type)) + if len(rvalue.args) > 2: + total = rvalue.args[2] else: - self.add(f"{self._indent}{lvalue.name}: Incomplete") - self.import_tracker.require_name("Incomplete") - return + for arg_name, arg in zip(rvalue.arg_names[1:], rvalue.args[1:]): + if not isinstance(arg_name, str): + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return + if arg_name == "total": + total = arg + else: + items.append((arg_name, arg)) self.import_tracker.require_name("TypedDict") p = AliasPrinter(self) if any(not key.isidentifier() or keyword.iskeyword(key) for key, _ in items): @@ -1123,8 +1138,9 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self._state = VAR else: bases = "TypedDict" - if len(rvalue.args) > 2: - bases += f", total={rvalue.args[2].accept(p)}" + # TODO: Add support for generic TypedDicts. Requires `Generic` as base class. + if total is not None: + bases += f", total={total.accept(p)}" self.add(f"{self._indent}class {lvalue.name}({bases}):") if len(items) == 0: self.add(" ...\n") diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 3f416b4f7745..f9a3ed93c3f1 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2801,6 +2801,22 @@ Y = typing.TypedDict('X', {'a': int, 'b': str}, total=False) [out] from typing_extensions import TypedDict +class X(TypedDict): + a: int + b: str + +class Y(TypedDict, total=False): + a: int + b: str + +[case testTypeddictKeywordSyntax] +from typing import TypedDict + +X = TypedDict('X', a=int, b=str) +Y = TypedDict('X', a=int, b=str, total=False) +[out] +from typing import TypedDict + class X(TypedDict): a: int b: str @@ -2826,11 +2842,20 @@ Z = TypedDict('X', {'a': int, 'in': str}) [case testEmptyTypeddict] import typing X = typing.TypedDict('X', {}) +Y = typing.TypedDict('Y', {}, total=False) +Z = typing.TypedDict('Z') +W = typing.TypedDict('W', total=False) [out] from typing_extensions import TypedDict class X(TypedDict): ... +class Y(TypedDict, total=False): ... + +class Z(TypedDict): ... + +class W(TypedDict, total=False): ... + [case testTypeddictWithUnderscore] from typing import TypedDict as _TypedDict def f(): ... From f06b01c87b8ff0c3601b726f7df39bea764b196a Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 2 Apr 2023 23:16:03 +0200 Subject: [PATCH 12/12] CR: more conservatism and typing_extensions test --- mypy/stubgen.py | 10 +++++++++- test-data/unit/stubgen.test | 24 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index a113683ea6a8..6a5c832704cd 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1111,7 +1111,11 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: items: list[tuple[str, Expression]] = [] total: Expression | None = None - if len(rvalue.args) > 1 and isinstance(rvalue.args[1], DictExpr): + if len(rvalue.args) > 1 and rvalue.arg_kinds[1] == ARG_POS: + if not isinstance(rvalue.args[1], DictExpr): + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return for attr_name, attr_type in rvalue.args[1].items: if not isinstance(attr_name, StrExpr): self.add(f"{self._indent}{lvalue.name}: Incomplete") @@ -1119,6 +1123,10 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: return items.append((attr_name.value, attr_type)) if len(rvalue.args) > 2: + if rvalue.arg_kinds[2] != ARG_NAMED or rvalue.arg_names[2] != "total": + self.add(f"{self._indent}{lvalue.name}: Incomplete") + self.import_tracker.require_name("Incomplete") + return total = rvalue.args[2] else: for arg_name, arg in zip(rvalue.arg_names[1:], rvalue.args[1:]): diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index f9a3ed93c3f1..9b3b8e3209b5 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -2856,10 +2856,12 @@ class Z(TypedDict): ... class W(TypedDict, total=False): ... -[case testTypeddictWithUnderscore] -from typing import TypedDict as _TypedDict +[case testTypeddictAliased] +from typing import TypedDict as t_TypedDict +from typing_extensions import TypedDict as te_TypedDict def f(): ... -X = _TypedDict('X', {'a': int, 'b': str}) +X = t_TypedDict('X', {'a': int, 'b': str}) +Y = te_TypedDict('Y', {'a': int, 'b': str}) def g(): ... [out] from typing_extensions import TypedDict @@ -2870,6 +2872,10 @@ class X(TypedDict): a: int b: str +class Y(TypedDict): + a: int + b: str + def g() -> None: ... [case testNotTypeddict] @@ -2885,8 +2891,18 @@ Y: Incomplete [case testTypeddictWithWrongAttributesType] from typing import TypedDict -T = TypeDict("T", {"a": int, **{"b": str, "c": bytes}}) +R = TypedDict("R", {"a": int, **{"b": str, "c": bytes}}) +S = TypedDict("S", [("b", str), ("c", bytes)]) +T = TypedDict("T", {"a": int}, b=str, total=False) +U = TypedDict("U", {"a": int}, totale=False) +V = TypedDict("V", {"a": int}, {"b": str}) +W = TypedDict("W", **{"a": int, "b": str}) [out] from _typeshed import Incomplete +R: Incomplete +S: Incomplete T: Incomplete +U: Incomplete +V: Incomplete +W: Incomplete