From 754d8f38366f4c7f31f56c269a36c9564fa1388c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 12 Jun 2024 12:19:09 -0700 Subject: [PATCH] Fix isinstance with PEP 604 type aliases Fixes #12155 and probably several duplicates --- 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 179ff6e0b4b68..ce14a68a3f5ba 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 479ef228b038f..be7d37a5372d7 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 2218a950788cc..d9bdf2e2b20b8 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 a6ae77832cebc..d0876629fc082 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 8f138ab5698f8..3670642ba8bc9 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 2cacc3e440850..22d017402a4bf 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 5ecc69dc7c324..4f28bd3b48a04 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 39357a693638b..a67f0f0660463 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 012fd85033779..e4869dbc30931 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: + ...