Skip to content

Commit

Permalink
Fix isinstance with type aliases to PEP 604 unions (#17371)
Browse files Browse the repository at this point in the history
Fixes #12155, fixes #11673, seems pretty commonly reported issue
  • Loading branch information
hauntsaninja authored Jun 13, 2024
1 parent 8fb9969 commit 98a22c4
Show file tree
Hide file tree
Showing 11 changed files with 53 additions and 3 deletions.
2 changes: 2 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion mypy/type_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-union-or-syntax.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions test-data/unit/fixtures/type.pyi
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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: ...
3 changes: 3 additions & 0 deletions test-data/unit/lib-stub/types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ if sys.version_info >= (3, 10):

class NoneType:
...

class UnionType:
...

0 comments on commit 98a22c4

Please sign in to comment.