From 98a22c44c26ff436f1c343ad4727258ffd72e055 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 13 Jun 2024 14:17:05 -0700 Subject: [PATCH] Fix isinstance with type aliases to PEP 604 unions (#17371) Fixes #12155, fixes #11673, seems pretty commonly reported issue --- 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-type-aliases.test | 1 + test-data/unit/check-union-or-syntax.test | 19 +++++++++++++++++++ test-data/unit/fine-grained.test | 1 + test-data/unit/fixtures/type.pyi | 7 +++++++ test-data/unit/lib-stub/types.pyi | 3 +++ 11 files changed, 53 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 70db31c9a94f..119aa9f3cea2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7323,6 +7323,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 c34952b084f9..861c28e5b54c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -527,6 +527,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(union_target := get_proper_type(node.node.target), UnionType) + and union_target.uses_pep604_syntax + ) ): self.msg.type_arguments_not_allowed(e) if isinstance(typ, RefExpr) and isinstance(typ.node, TypeInfo): @@ -4762,6 +4766,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 82c90272d6c2..6651af7dad4f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1271,7 +1271,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 cdcb26f435b8..0f8c48c8cb7d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2821,6 +2821,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-type-aliases.test b/test-data/unit/check-type-aliases.test index 86bd4422003b..5eea1fb2b53e 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -967,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 85e268f348f0..b5fd85cb7ed8 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -207,6 +207,25 @@ foo: ReadableBuffer [file was_mmap.pyi] from was_builtins import * class mmap: ... +[builtins fixtures/type.pyi] + +[case testTypeAliasWithNewUnionIsInstance] +# flags: --python-version 3.10 +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] + # TODO: Get this test to pass [case testImplicit604TypeAliasWithCyclicImportNotInStub-xfail] 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 diff --git a/test-data/unit/fixtures/type.pyi b/test-data/unit/fixtures/type.pyi index 39357a693638..084b7f8388d8 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,8 @@ class bool: pass class int: pass class str: pass class ellipsis: pass + +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: ... 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: + ...