Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections import defaultdict
from collections.abc import Iterable, Iterator, Mapping, Sequence, Set as AbstractSet
from contextlib import ExitStack, contextmanager
from functools import reduce
from typing import (
Callable,
Final,
Expand Down Expand Up @@ -6231,9 +6232,13 @@ def find_type_equals_check(
compared
"""

def is_type_call(expr: CallExpr) -> bool:
def is_type_call(expr: Expression) -> TypeGuard[CallExpr]:
"""Is expr a call to type with one argument?"""
return refers_to_fullname(expr.callee, "builtins.type") and len(expr.args) == 1
return (
isinstance(expr, CallExpr)
and refers_to_fullname(expr.callee, "builtins.type")
and len(expr.args) == 1
)

# exprs that are being passed into type
exprs_in_type_calls: list[Expression] = []
Expand All @@ -6245,13 +6250,13 @@ def is_type_call(expr: CallExpr) -> bool:
for index in expr_indices:
expr = node.operands[index]

if isinstance(expr, CallExpr) and is_type_call(expr):
if is_type_call(expr):
exprs_in_type_calls.append(expr.args[0])
else:
current_type = self.get_isinstance_type(expr)
if current_type is None:
continue
if type_being_compared is not None:
if type_being_compared is not None and type_being_compared != current_type:
# It doesn't really make sense to have several types being
# compared to the output of type (like type(x) == int == str)
# because whether that's true is solely dependent on what the
Expand All @@ -6267,6 +6272,13 @@ def is_type_call(expr: CallExpr) -> bool:
if not exprs_in_type_calls:
return {}, {}

if type_being_compared is None:
# TODO: use more accurate lower bound analysis
least_type = self.least_type([self.lookup_type(expr) for expr in exprs_in_type_calls])
type_being_compared = (
None if least_type is None else [TypeRange(least_type, is_upper_bound=True)]
)

if_maps: list[TypeMap] = []
else_maps: list[TypeMap] = []
for expr in exprs_in_type_calls:
Expand Down Expand Up @@ -6300,6 +6312,28 @@ def combine_maps(list_maps: list[TypeMap]) -> TypeMap:
else_map = {}
return if_map, else_map

def least_type(self, types: list[Type]) -> Type | None:
"""Find the type of which all other types are supertypes.
`Any` types are i
For example, `least_type([dict, defaultdict, object]) => dict`.
However, `least_type[int, str]) => None`.

It would be better if we could represent an intersection of types.

For example, consider `s: str` and `i: int`.
`type(s) == type(i)` implies `s: str & int` and `i: str & int`,
even though `s: object` and `i: object` also hold.
"""
types = [typ for typ in types if not isinstance(typ, AnyType)]
if not types:
return None
least_type = reduce(lambda t1, t2: t1 if is_subtype(t1, t2) else t2, types)

if all(typ is least_type or is_subtype(least_type, typ) for typ in types):
# Ensure that this is a least type
return least_type
return None

def find_isinstance_check(
self, node: Expression, *, in_boolean_context: bool = True
) -> tuple[TypeMap, TypeMap]:
Expand Down
30 changes: 30 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -2716,13 +2716,43 @@ if type(x) == type(y) == int:
reveal_type(y) # N: Revealed type is "builtins.int"
reveal_type(x) # N: Revealed type is "builtins.int"

z: Any
if int == type(z) == int:
reveal_type(z) # N: Revealed type is "builtins.int"

[case testTypeEqualsCheckUsingIs]
from typing import Any

y: Any
if type(y) is int:
reveal_type(y) # N: Revealed type is "builtins.int"

[case testTypeEqualsCheckUsingImplicitTypes]
from typing import Any

x: str
y: Any
z: object
if type(y) is type(x):
reveal_type(x) # N: Revealed type is "builtins.str"
reveal_type(y) # N: Revealed type is "builtins.str"

if type(x) is type(z):
reveal_type(x) # N: Revealed type is "builtins.str"
reveal_type(z) # N: Revealed type is "builtins.str"

[case testTypeEqualsCheckUsingDifferentSpecializedTypes]
from collections import defaultdict

x: defaultdict
y: dict
z: object
if type(x) is type(y) is type(z):
reveal_type(x) # N: Revealed type is "collections.defaultdict[Any, Any]"
reveal_type(y) # N: Revealed type is "collections.defaultdict[Any, Any]"
reveal_type(z) # N: Revealed type is "collections.defaultdict[Any, Any]"


[case testTypeEqualsCheckUsingIsNonOverlapping]
# flags: --warn-unreachable
from typing import Union
Expand Down
Loading