From ea1882ac0384d262dcd7677b392429f32f7f9210 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 11:33:50 +0200 Subject: [PATCH 1/6] Gracefully handle invalid Optional usage in stubgen --- mypy/stubgen.py | 2 ++ mypy/stubutil.py | 4 +++- test-data/unit/stubgen.test | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 22028694ad6b..ceafbe267621 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -314,6 +314,8 @@ def visit_index_expr(self, node: IndexExpr) -> str: return " | ".join([item.accept(self) for item in node.index.items]) return node.index.accept(self) if base_fullname == "typing.Optional": + if isinstance(node.index, TupleExpr): + return " | ".join([item.accept(self) for item in node.index.items] + ["None"]) return f"{node.index.accept(self)} | None" base = node.base.accept(self) index = node.index.accept(self) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 8e41d6862531..c316bc391c5f 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -257,7 +257,9 @@ def visit_unbound_type(self, t: UnboundType) -> str: if fullname == "typing.Union": return " | ".join([item.accept(self) for item in t.args]) if fullname == "typing.Optional": - return f"{t.args[0].accept(self)} | None" + if not t.args: + return self.stubgen.add_name("_typeshed.Incomplete") + return " | ".join([item.accept(self) for item in t.args] + ["None"]) if fullname in TYPING_BUILTIN_REPLACEMENTS: s = self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=True) if self.known_modules is not None and "." in s: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 916e2e3a8e17..65a1db9a7b9e 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4366,3 +4366,25 @@ class Foo(Enum): class Bar(Enum): A = ... B = ... + +[case testGracefullyHandleInvalidOptionalUsage] +from typing import Optional + +x: Optional +y: Optional[int] +z: Optional[int, str] + +X = Optional +Y = Optional[int] +Z = Optional[int, str] + +[out] +from _typeshed import Incomplete +from typing import Optional + +x: Incomplete +y: int | None +z: int | str | None +X = Optional +Y = int | None +Z = int | str | None From 606353efa91ee8989eec2f1b6508ebe163fa561a Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 11:39:18 +0200 Subject: [PATCH 2/6] Even better type for empty Optional --- mypy/stubutil.py | 2 +- test-data/unit/stubgen.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index c316bc391c5f..17b682e27a59 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -258,7 +258,7 @@ def visit_unbound_type(self, t: UnboundType) -> str: return " | ".join([item.accept(self) for item in t.args]) if fullname == "typing.Optional": if not t.args: - return self.stubgen.add_name("_typeshed.Incomplete") + return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" return " | ".join([item.accept(self) for item in t.args] + ["None"]) if fullname in TYPING_BUILTIN_REPLACEMENTS: s = self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=True) diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 65a1db9a7b9e..4de32bd961d8 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4382,7 +4382,7 @@ Z = Optional[int, str] from _typeshed import Incomplete from typing import Optional -x: Incomplete +x: Incomplete | None y: int | None z: int | str | None X = Optional From 90176d01a294e48c732768071c775dd32416f92c Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 17:28:11 +0200 Subject: [PATCH 3/6] No guessing --- mypy/stubgen.py | 2 +- mypy/stubutil.py | 6 +++--- test-data/unit/stubgen.test | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index ceafbe267621..cc62213e90d2 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -315,7 +315,7 @@ def visit_index_expr(self, node: IndexExpr) -> str: return node.index.accept(self) if base_fullname == "typing.Optional": if isinstance(node.index, TupleExpr): - return " | ".join([item.accept(self) for item in node.index.items] + ["None"]) + return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" return f"{node.index.accept(self)} | None" base = node.base.accept(self) index = node.index.accept(self) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 17b682e27a59..3abd0a985f34 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -257,9 +257,9 @@ def visit_unbound_type(self, t: UnboundType) -> str: if fullname == "typing.Union": return " | ".join([item.accept(self) for item in t.args]) if fullname == "typing.Optional": - if not t.args: - return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" - return " | ".join([item.accept(self) for item in t.args] + ["None"]) + if len(t.args) == 1: + return f"{t.args[0].accept(self)} | None" + return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" if fullname in TYPING_BUILTIN_REPLACEMENTS: s = self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=True) if self.known_modules is not None and "." in s: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 4de32bd961d8..92a100073669 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4384,7 +4384,7 @@ from typing import Optional x: Incomplete | None y: int | None -z: int | str | None +z: Incomplete | None X = Optional Y = int | None -Z = int | str | None +Z = Incomplete | None From ac2224f86919d76a75c6e84d9036d515f8c98e67 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 18:08:34 +0200 Subject: [PATCH 4/6] Fix and test PEP 604 unions --- mypy/stubgen.py | 4 ++++ test-data/unit/stubgen.test | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index cc62213e90d2..6cd3512ede85 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1062,6 +1062,10 @@ def is_alias_expression(self, expr: Expression, top_level: bool = True) -> bool: else: return False return all(self.is_alias_expression(i, top_level=False) for i in indices) + elif isinstance(expr, OpExpr) and expr.op == "|": + return self.is_alias_expression( + expr.left, top_level=False + ) and self.is_alias_expression(expr.right, top_level=False) else: return False diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 92a100073669..3d6b205bfc76 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4370,13 +4370,15 @@ class Bar(Enum): [case testGracefullyHandleInvalidOptionalUsage] from typing import Optional -x: Optional -y: Optional[int] -z: Optional[int, str] +x: Optional # invalid +y: Optional[int] # valid +z: Optional[int, str] # invalid +w: Optional[int | str] # valid X = Optional Y = Optional[int] Z = Optional[int, str] +W = Optional[int | str] [out] from _typeshed import Incomplete @@ -4385,6 +4387,8 @@ from typing import Optional x: Incomplete | None y: int | None z: Incomplete | None +w: int | str | None X = Optional Y = int | None Z = Incomplete | None +W = int | str | None From 39d8549f35bc8dddb0b98f2ba4af1434572cf8d7 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 18:14:35 +0200 Subject: [PATCH 5/6] Listen to Alex --- mypy/stubgen.py | 2 +- mypy/stubutil.py | 2 +- test-data/unit/stubgen.test | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 6cd3512ede85..8478bd2135e4 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -315,7 +315,7 @@ def visit_index_expr(self, node: IndexExpr) -> str: return node.index.accept(self) if base_fullname == "typing.Optional": if isinstance(node.index, TupleExpr): - return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" + return self.stubgen.add_name("_typeshed.Incomplete") return f"{node.index.accept(self)} | None" base = node.base.accept(self) index = node.index.accept(self) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 3abd0a985f34..2f2db0dbbe53 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -259,7 +259,7 @@ def visit_unbound_type(self, t: UnboundType) -> str: if fullname == "typing.Optional": if len(t.args) == 1: return f"{t.args[0].accept(self)} | None" - return f"{self.stubgen.add_name('_typeshed.Incomplete')} | None" + return self.stubgen.add_name("_typeshed.Incomplete") if fullname in TYPING_BUILTIN_REPLACEMENTS: s = self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=True) if self.known_modules is not None and "." in s: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 3d6b205bfc76..ca5910ac3fb3 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4384,11 +4384,11 @@ W = Optional[int | str] from _typeshed import Incomplete from typing import Optional -x: Incomplete | None +x: Incomplete y: int | None -z: Incomplete | None +z: Incomplete w: int | str | None X = Optional Y = int | None -Z = Incomplete | None +Z = Incomplete W = int | str | None From 74c1e77f8dd6bae9d29d11dd0238bce61104fa5a Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 15 Jun 2024 18:20:43 +0200 Subject: [PATCH 6/6] One more exotic test --- test-data/unit/stubgen.test | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index ca5910ac3fb3..5dcb0706a8cb 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4374,11 +4374,13 @@ x: Optional # invalid y: Optional[int] # valid z: Optional[int, str] # invalid w: Optional[int | str] # valid +r: Optional[type[int | str]] X = Optional Y = Optional[int] Z = Optional[int, str] W = Optional[int | str] +R = Optional[type[int | str]] [out] from _typeshed import Incomplete @@ -4388,7 +4390,9 @@ x: Incomplete y: int | None z: Incomplete w: int | str | None +r: type[int | str] | None X = Optional Y = int | None Z = Incomplete W = int | str | None +R = type[int | str] | None