From 0fa9f0c9a1285a82806d2a1f8b2eed9bb729b5b3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 12 Jun 2024 12:19:09 -0700 Subject: [PATCH 1/4] Fix isinstance with PEP 604 type aliases Fixes #12155, fixes #11673 --- mypy/checker.py | 2 ++ mypy/checkexpr.py | 10 ++++++++++ mypy/exprtotype.py | 3 ++- mypy/type_visitor.py | 7 ++++++- mypy/typeanal.py | 2 +- mypy/types.py | 1 + test-data/unit/check-python310.test | 16 ++++++++++++++++ test-data/unit/fixtures/type.pyi | 4 ++++ test-data/unit/lib-stub/types.pyi | 3 +++ 9 files changed, 45 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 179ff6e0b4b6..ce14a68a3f5b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7295,6 +7295,8 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type": object_type = Instance(typ.type.mro[-1], []) types.append(TypeRange(object_type, is_upper_bound=True)) + elif isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args: + types.append(TypeRange(UnionType(typ.args), is_upper_bound=False)) elif isinstance(typ, AnyType): types.append(TypeRange(typ, is_upper_bound=False)) else: # we didn't see an actual type, but rather a variable with unknown value diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 479ef228b038..be7d37a5372d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -529,6 +529,10 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> and node and isinstance(node.node, TypeAlias) and not node.node.no_args + and not ( + isinstance(node.node.target, UnionType) + and node.node.target.uses_pep604_syntax + ) ): self.msg.type_arguments_not_allowed(e) if isinstance(typ, RefExpr) and isinstance(typ.node, TypeInfo): @@ -4760,6 +4764,12 @@ class LongName(Generic[T]): ... return TypeType(item, line=item.line, column=item.column) elif isinstance(item, AnyType): return AnyType(TypeOfAny.from_another_any, source_any=item) + elif ( + isinstance(item, UnionType) + and item.uses_pep604_syntax + and self.chk.options.python_version >= (3, 10) + ): + return self.chk.named_generic_type("types.UnionType", item.items) else: if alias_definition: return AnyType(TypeOfAny.special_form) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 2218a950788c..d9bdf2e2b20b 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -122,7 +122,8 @@ def expr_to_unanalyzed_type( [ expr_to_unanalyzed_type(expr.left, options, allow_new_syntax), expr_to_unanalyzed_type(expr.right, options, allow_new_syntax), - ] + ], + uses_pep604_syntax=True, ) elif isinstance(expr, CallExpr) and isinstance(_parent, ListExpr): c = expr.callee diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index a6ae77832ceb..d0876629fc08 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -266,7 +266,12 @@ def visit_literal_type(self, t: LiteralType) -> Type: return LiteralType(value=t.value, fallback=fallback, line=t.line, column=t.column) def visit_union_type(self, t: UnionType) -> Type: - return UnionType(self.translate_types(t.items), t.line, t.column) + return UnionType( + self.translate_types(t.items), + t.line, + t.column, + uses_pep604_syntax=t.uses_pep604_syntax, + ) def translate_types(self, types: Iterable[Type]) -> list[Type]: return [t.accept(self) for t in types] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8f138ab5698f..3670642ba8bc 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1248,7 +1248,7 @@ def visit_union_type(self, t: UnionType) -> Type: and not self.options.python_version >= (3, 10) ): self.fail("X | Y syntax for unions requires Python 3.10", t, code=codes.SYNTAX) - return UnionType(self.anal_array(t.items), t.line) + return UnionType(self.anal_array(t.items), t.line, uses_pep604_syntax=t.uses_pep604_syntax) def visit_partial_type(self, t: PartialType) -> Type: assert False, "Internal error: Unexpected partial type" diff --git a/mypy/types.py b/mypy/types.py index 2cacc3e44085..22d017402a4b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2822,6 +2822,7 @@ def __init__( items: Sequence[Type], line: int = -1, column: int = -1, + *, is_evaluated: bool = True, uses_pep604_syntax: bool = False, ) -> None: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 5ecc69dc7c32..4f28bd3b48a0 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1983,6 +1983,22 @@ X = None | C Y = None | D [builtins fixtures/type.pyi] +[case testTypeAliasWithNewUnionIsInstance] +SimpleAlias = int | str + +def foo(x: int | str | tuple): + if isinstance(x, SimpleAlias): + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + else: + reveal_type(x) # N: Revealed type is "builtins.tuple[Any, ...]" + +ParameterizedAlias = str | list[str] + +# these are false negatives: +isinstance(5, str | list[str]) +isinstance(5, ParameterizedAlias) +[builtins fixtures/type.pyi] + [case testMatchStatementWalrus] class A: a = 1 diff --git a/test-data/unit/fixtures/type.pyi b/test-data/unit/fixtures/type.pyi index 39357a693638..a67f0f066046 100644 --- a/test-data/unit/fixtures/type.pyi +++ b/test-data/unit/fixtures/type.pyi @@ -1,6 +1,8 @@ # builtins stub used in type-related test cases. from typing import Any, Generic, TypeVar, List, Union +import sys +import types T = TypeVar("T") S = TypeVar("S") @@ -25,3 +27,5 @@ class bool: pass class int: pass class str: pass class ellipsis: pass + +def isinstance(obj: object, class_or_tuple: type | types.UnionType, /) -> bool: ... diff --git a/test-data/unit/lib-stub/types.pyi b/test-data/unit/lib-stub/types.pyi index 012fd8503377..e4869dbc3093 100644 --- a/test-data/unit/lib-stub/types.pyi +++ b/test-data/unit/lib-stub/types.pyi @@ -15,3 +15,6 @@ if sys.version_info >= (3, 10): class NoneType: ... + + class UnionType: + ... From bd96ef6ba3e46eeca5c8f1afb3c8ab96495c18b7 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 12 Jun 2024 13:07:09 -0700 Subject: [PATCH 2/4] . --- mypy/checkexpr.py | 4 ++-- test-data/unit/check-type-aliases.test | 1 + test-data/unit/check-union-or-syntax.test | 1 + test-data/unit/fixtures/type.pyi | 5 ++++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index be7d37a5372d..7f8f391b1eae 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -530,8 +530,8 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> and isinstance(node.node, TypeAlias) and not node.node.no_args and not ( - isinstance(node.node.target, UnionType) - and node.node.target.uses_pep604_syntax + isinstance(union_target := get_proper_type(node.node.target), UnionType) + and union_target.uses_pep604_syntax ) ): self.msg.type_arguments_not_allowed(e) diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index f77c3c1c34e2..a1bc8a4a8c60 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -959,6 +959,7 @@ c.SpecialExplicit = 4 [case testNewStyleUnionInTypeAliasWithMalformedInstance] # flags: --python-version 3.10 +import types from typing import List A = List[int, str] | int # E: "list" expects 1 type argument, but 2 given diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 85e268f348f0..2fd6f933f314 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -197,6 +197,7 @@ def f(x: Union[int, str, None]) -> None: [case testImplicit604TypeAliasWithCyclicImportInStub] # flags: --python-version 3.10 +import types from was_builtins import foo reveal_type(foo) # N: Revealed type is "Union[builtins.str, was_mmap.mmap]" [file was_builtins.pyi] diff --git a/test-data/unit/fixtures/type.pyi b/test-data/unit/fixtures/type.pyi index a67f0f066046..084b7f8388d8 100644 --- a/test-data/unit/fixtures/type.pyi +++ b/test-data/unit/fixtures/type.pyi @@ -28,4 +28,7 @@ class int: pass class str: pass class ellipsis: pass -def isinstance(obj: object, class_or_tuple: type | types.UnionType, /) -> bool: ... +if sys.version_info >= (3, 10): # type: ignore + def isinstance(obj: object, class_or_tuple: type | types.UnionType, /) -> bool: ... +else: + def isinstance(obj: object, class_or_tuple: type, /) -> bool: ... From e037a30d603c3f31cbbb6e2536a87ef320f60460 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 12 Jun 2024 13:29:58 -0700 Subject: [PATCH 3/4] . --- test-data/unit/check-python310.test | 16 ---------------- test-data/unit/check-type-aliases.test | 2 +- test-data/unit/check-union-or-syntax.test | 18 +++++++++++++++++- test-data/unit/fine-grained.test | 1 + 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 4f28bd3b48a0..5ecc69dc7c32 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1983,22 +1983,6 @@ X = None | C Y = None | D [builtins fixtures/type.pyi] -[case testTypeAliasWithNewUnionIsInstance] -SimpleAlias = int | str - -def foo(x: int | str | tuple): - if isinstance(x, SimpleAlias): - reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" - else: - reveal_type(x) # N: Revealed type is "builtins.tuple[Any, ...]" - -ParameterizedAlias = str | list[str] - -# these are false negatives: -isinstance(5, str | list[str]) -isinstance(5, ParameterizedAlias) -[builtins fixtures/type.pyi] - [case testMatchStatementWalrus] class A: a = 1 diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index a1bc8a4a8c60..047097b2a8b5 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -959,7 +959,6 @@ c.SpecialExplicit = 4 [case testNewStyleUnionInTypeAliasWithMalformedInstance] # flags: --python-version 3.10 -import types from typing import List A = List[int, str] | int # E: "list" expects 1 type argument, but 2 given @@ -968,6 +967,7 @@ a: A b: B reveal_type(a) # N: Revealed type is "Union[builtins.list[Any], builtins.int]" reveal_type(b) # N: Revealed type is "Union[builtins.int, builtins.list[Any]]" +[builtins fixtures/type.pyi] [case testValidTypeAliasValues] from typing import TypeVar, Generic, List diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 2fd6f933f314..69a5a51b1b3d 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -197,7 +197,6 @@ def f(x: Union[int, str, None]) -> None: [case testImplicit604TypeAliasWithCyclicImportInStub] # flags: --python-version 3.10 -import types from was_builtins import foo reveal_type(foo) # N: Revealed type is "Union[builtins.str, was_mmap.mmap]" [file was_builtins.pyi] @@ -208,6 +207,7 @@ foo: ReadableBuffer [file was_mmap.pyi] from was_builtins import * class mmap: ... +[builtins fixtures/type.pyi] # TODO: Get this test to pass [case testImplicit604TypeAliasWithCyclicImportNotInStub-xfail] @@ -222,3 +222,19 @@ foo: ReadableBuffer [file was_mmap.py] from was_builtins import * class mmap: ... + +[case testTypeAliasWithNewUnionIsInstance] +SimpleAlias = int | str + +def foo(x: int | str | tuple): + if isinstance(x, SimpleAlias): + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + else: + reveal_type(x) # N: Revealed type is "builtins.tuple[Any, ...]" + +ParameterizedAlias = str | list[str] + +# these are false negatives: +isinstance(5, str | list[str]) +isinstance(5, ParameterizedAlias) +[builtins fixtures/type.pyi] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 9c379d8f60da..a87f8ceca15c 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10380,6 +10380,7 @@ from b import C, D A = C | D a: A reveal_type(a) +[builtins fixtures/type.pyi] [file b.py] C = int From 2084372190e0b8d86cf4d1118705eb4cb6dbaa67 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 12 Jun 2024 14:09:14 -0700 Subject: [PATCH 4/4] . --- test-data/unit/check-union-or-syntax.test | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 69a5a51b1b3d..b5fd85cb7ed8 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -209,21 +209,8 @@ from was_builtins import * class mmap: ... [builtins fixtures/type.pyi] -# TODO: Get this test to pass -[case testImplicit604TypeAliasWithCyclicImportNotInStub-xfail] -# flags: --python-version 3.10 -from was_builtins import foo -reveal_type(foo) # N: Revealed type is "Union[builtins.str, was_mmap.mmap]" -[file was_builtins.py] -import was_mmap -WriteableBuffer = was_mmap.mmap -ReadableBuffer = str | WriteableBuffer -foo: ReadableBuffer -[file was_mmap.py] -from was_builtins import * -class mmap: ... - [case testTypeAliasWithNewUnionIsInstance] +# flags: --python-version 3.10 SimpleAlias = int | str def foo(x: int | str | tuple): @@ -238,3 +225,18 @@ ParameterizedAlias = str | list[str] isinstance(5, str | list[str]) isinstance(5, ParameterizedAlias) [builtins fixtures/type.pyi] + + +# TODO: Get this test to pass +[case testImplicit604TypeAliasWithCyclicImportNotInStub-xfail] +# flags: --python-version 3.10 +from was_builtins import foo +reveal_type(foo) # N: Revealed type is "Union[builtins.str, was_mmap.mmap]" +[file was_builtins.py] +import was_mmap +WriteableBuffer = was_mmap.mmap +ReadableBuffer = str | WriteableBuffer +foo: ReadableBuffer +[file was_mmap.py] +from was_builtins import * +class mmap: ...