-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
stubgen: Support TypedDict alternative syntax #14682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
56666e6
560e744
3f2c434
0e617b6
edeba78
444b3f4
5a85e1d
1bd4c13
fe32c21
a6fbd4a
d9cc7f5
b42f9b6
f06b01c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ | |
|
|
||
| import argparse | ||
| import glob | ||
| import keyword | ||
| import os | ||
| import os.path | ||
| import sys | ||
|
|
@@ -80,6 +81,7 @@ | |
| ClassDef, | ||
| ComparisonExpr, | ||
| Decorator, | ||
| DictExpr, | ||
| EllipsisExpr, | ||
| Expression, | ||
| FloatExpr, | ||
|
|
@@ -126,6 +128,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, | ||
|
|
@@ -405,6 +408,14 @@ def visit_tuple_expr(self, node: TupleExpr) -> str: | |
| 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: | ||
| 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 "..." | ||
|
|
||
|
|
@@ -623,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)]) | ||
| 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: | ||
|
|
@@ -1003,6 +1014,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) | ||
|
hamdanal marked this conversation as resolved.
Outdated
|
||
| self.process_typeddict(lvalue, o.rvalue) | ||
| continue | ||
| if ( | ||
| isinstance(lvalue, NameExpr) | ||
| and not self.is_private_name(lvalue.name) | ||
|
|
@@ -1071,6 +1086,53 @@ 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) 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: | ||
| if self._state != EMPTY: | ||
| self.add("\n") | ||
| if isinstance(rvalue.args[1], DictExpr): | ||
| 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") | ||
| return | ||
| self.import_tracker.require_name("TypedDict") | ||
| p = AliasPrinter(self) | ||
| 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") | ||
|
hamdanal marked this conversation as resolved.
|
||
| self._state = VAR | ||
| else: | ||
| bases = "TypedDict" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mypy actually now supports generic TypedDicts defined with the call syntax, in which case I think it's reasonable not to handle generic TypedDicts defined using the call syntax in this PR, since they're unlikely to come up much. But probably worth a TODO comment?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL, thank you. |
||
| if len(rvalue.args) > 2: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if a user does something like Foo = TypedDict("Foo", {"a": int}, b=str, d=bytes)Defining TypedDicts like this using keyword arguments is deprecated at runtime, and has never been supported by mypy — but according to the typing docs it is (or was until recently, when we deprecated it) a supported way of creating a new TypedDict type: https://docs.python.org/3/library/typing.html#typing.TypedDict
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right again Alex. Indeed I forgot about the keyword syntax. Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import TypedDict
>>> Foo = TypedDict("Foo", {"a": int}, b=str, d=bytes)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.10/typing.py", line 2435, in TypedDict
raise TypeError("TypedDict takes either a dict or keyword arguments,"
TypeError: TypedDict takes either a dict or keyword arguments, but not boththis works: >>> from typing import TypedDict
>>> Foo = TypedDict("Foo", a=int, b=str, d=bytes)
>>> Foo.__required_keys__
frozenset({'b', 'd', 'a'})
>>> Foo.__optional_keys__
frozenset()I'll update the PR to add support for the keyword syntax. |
||
| bases += f", total={rvalue.args[2].accept(p)}" | ||
| 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 | ||
|
|
||
| def is_alias_expression(self, expr: Expression, top_level: bool = True) -> bool: | ||
| """Return True for things that look like target for an alias. | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.