blob: a3b59c230af25c24b45c38a41e9549dc9f076b69 [file] [log] [blame]
from collections import OrderedDict
from typing import List, Optional, cast, Tuple
from mypy.join import is_similar_callables, combine_similar_callables, join_type_list
from mypy.types import (
Type, AnyType, TypeVisitor, UnboundType, NoneTyp, TypeVarType,
Instance, CallableType, TupleType, TypedDictType, ErasedType, TypeList, UnionType, PartialType,
DeletedType, UninhabitedType, TypeType
)
from mypy.subtypes import is_equivalent, is_subtype
from mypy import experiments
# TODO Describe this module.
def meet_types(s: Type, t: Type) -> Type:
"""Return the greatest lower bound of two types."""
if isinstance(s, ErasedType):
return s
if isinstance(s, AnyType):
return t
if isinstance(s, UnionType) and not isinstance(t, UnionType):
s, t = t, s
return t.accept(TypeMeetVisitor(s))
def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
"""Return the declared type narrowed down to another type."""
if declared == narrowed:
return declared
if isinstance(declared, UnionType):
return UnionType.make_simplified_union([narrow_declared_type(x, narrowed)
for x in declared.relevant_items()])
elif not is_overlapping_types(declared, narrowed, use_promotions=True):
if experiments.STRICT_OPTIONAL:
return UninhabitedType()
else:
return NoneTyp()
elif isinstance(narrowed, UnionType):
return UnionType.make_simplified_union([narrow_declared_type(declared, x)
for x in narrowed.relevant_items()])
elif isinstance(narrowed, AnyType):
return narrowed
elif isinstance(declared, (Instance, TupleType)):
return meet_types(declared, narrowed)
return narrowed
def is_overlapping_types(t: Type, s: Type, use_promotions: bool = False) -> bool:
"""Can a value of type t be a value of type s, or vice versa?
Note that this effectively checks against erased types, since type
variables are erased at runtime and the overlapping check is based
on runtime behavior.
If use_promotions is True, also consider type promotions (int and
float would only be overlapping if it's True).
This does not consider multiple inheritance. For example, A and B in
the following example are not considered overlapping, even though
via C they can be overlapping:
class A: ...
class B: ...
class C(A, B): ...
The rationale is that this case is usually very unlikely as multiple
inheritance is rare. Also, we can't reliably determine whether
multiple inheritance actually occurs somewhere in a program, due to
stub files hiding implementation details, dynamic loading etc.
TODO: Don't consider tuples always overlapping.
TODO: Don't consider callables always overlapping.
TODO: Don't consider type variables with values always overlapping.
"""
# Any overlaps with everything
if isinstance(t, AnyType) or isinstance(s, AnyType):
return True
# Since we are effectively working with the erased types, we only
# need to handle occurrences of TypeVarType at the top level.
if isinstance(t, TypeVarType):
t = t.erase_to_union_or_bound()
if isinstance(s, TypeVarType):
s = s.erase_to_union_or_bound()
if isinstance(t, TypedDictType):
t = t.as_anonymous().fallback
if isinstance(s, TypedDictType):
s = s.as_anonymous().fallback
if isinstance(t, Instance):
if isinstance(s, Instance):
# Consider two classes non-disjoint if one is included in the mro
# of another.
if use_promotions:
# Consider cases like int vs float to be overlapping where
# there is only a type promotion relationship but not proper
# subclassing.
if t.type._promote and is_overlapping_types(t.type._promote, s):
return True
if s.type._promote and is_overlapping_types(s.type._promote, t):
return True
return t.type in s.type.mro or s.type in t.type.mro
if isinstance(t, UnionType):
return any(is_overlapping_types(item, s)
for item in t.relevant_items())
if isinstance(s, UnionType):
return any(is_overlapping_types(t, item)
for item in s.relevant_items())
if isinstance(t, TypeType) and isinstance(s, TypeType):
# If both types are TypeType, compare their inner types.
return is_overlapping_types(t.item, s.item, use_promotions)
elif isinstance(t, TypeType) or isinstance(s, TypeType):
# If exactly only one of t or s is a TypeType, check if one of them
# is an `object` or a `type` and otherwise assume no overlap.
one = t if isinstance(t, TypeType) else s
other = s if isinstance(t, TypeType) else t
if isinstance(other, Instance):
return other.type.fullname() in {'builtins.object', 'builtins.type'}
else:
return isinstance(other, CallableType) and is_subtype(other, one)
if experiments.STRICT_OPTIONAL:
if isinstance(t, NoneTyp) != isinstance(s, NoneTyp):
# NoneTyp does not overlap with other non-Union types under strict Optional checking
return False
# We conservatively assume that non-instance, non-union, and non-TypeType types can overlap
# any other types.
return True
class TypeMeetVisitor(TypeVisitor[Type]):
def __init__(self, s: Type) -> None:
self.s = s
def visit_unbound_type(self, t: UnboundType) -> Type:
if isinstance(self.s, NoneTyp):
if experiments.STRICT_OPTIONAL:
return AnyType()
else:
return self.s
elif isinstance(self.s, UninhabitedType):
return self.s
else:
return AnyType()
def visit_any(self, t: AnyType) -> Type:
return self.s
def visit_union_type(self, t: UnionType) -> Type:
if isinstance(self.s, UnionType):
meets = [] # type: List[Type]
for x in t.items:
for y in self.s.items:
meets.append(meet_types(x, y))
else:
meets = [meet_types(x, self.s)
for x in t.items]
return UnionType.make_simplified_union(meets)
def visit_none_type(self, t: NoneTyp) -> Type:
if experiments.STRICT_OPTIONAL:
if isinstance(self.s, NoneTyp) or (isinstance(self.s, Instance) and
self.s.type.fullname() == 'builtins.object'):
return t
else:
return UninhabitedType()
else:
return t
def visit_uninhabited_type(self, t: UninhabitedType) -> Type:
return t
def visit_deleted_type(self, t: DeletedType) -> Type:
if isinstance(self.s, NoneTyp):
if experiments.STRICT_OPTIONAL:
return t
else:
return self.s
elif isinstance(self.s, UninhabitedType):
return self.s
else:
return t
def visit_erased_type(self, t: ErasedType) -> Type:
return self.s
def visit_type_var(self, t: TypeVarType) -> Type:
if isinstance(self.s, TypeVarType) and self.s.id == t.id:
return self.s
else:
return self.default(self.s)
def visit_instance(self, t: Instance) -> Type:
if isinstance(self.s, Instance):
si = self.s
if t.type == si.type:
if is_subtype(t, self.s) or is_subtype(self.s, t):
# Combine type arguments. We could have used join below
# equivalently.
args = [] # type: List[Type]
for i in range(len(t.args)):
args.append(self.meet(t.args[i], si.args[i]))
return Instance(t.type, args)
else:
if experiments.STRICT_OPTIONAL:
return UninhabitedType()
else:
return NoneTyp()
else:
if is_subtype(t, self.s):
return t
elif is_subtype(self.s, t):
# See also above comment.
return self.s
else:
if experiments.STRICT_OPTIONAL:
return UninhabitedType()
else:
return NoneTyp()
elif isinstance(self.s, TypeType):
return meet_types(t, self.s)
elif isinstance(self.s, TupleType):
return meet_types(t, self.s)
else:
return self.default(self.s)
def visit_callable_type(self, t: CallableType) -> Type:
if isinstance(self.s, CallableType) and is_similar_callables(t, self.s):
if is_equivalent(t, self.s):
return combine_similar_callables(t, self.s)
result = meet_similar_callables(t, self.s)
if isinstance(result.ret_type, UninhabitedType):
# Return a plain None or <uninhabited> instead of a weird function.
return self.default(self.s)
return result
else:
return self.default(self.s)
def visit_tuple_type(self, t: TupleType) -> Type:
if isinstance(self.s, TupleType) and self.s.length() == t.length():
items = [] # type: List[Type]
for i in range(t.length()):
items.append(self.meet(t.items[i], self.s.items[i]))
# TODO: What if the fallbacks are different?
return TupleType(items, t.fallback)
# meet(Tuple[t1, t2, <...>], Tuple[s, ...]) == Tuple[meet(t1, s), meet(t2, s), <...>].
elif (isinstance(self.s, Instance) and
self.s.type.fullname() == 'builtins.tuple' and self.s.args):
return t.copy_modified(items=[meet_types(it, self.s.args[0]) for it in t.items])
elif (isinstance(self.s, Instance) and t.fallback.type == self.s.type):
# Uh oh, a broken named tuple type (https://github.com/python/mypy/issues/3016).
# Do something reasonable until that bug is fixed.
return t
else:
return self.default(self.s)
def visit_typeddict_type(self, t: TypedDictType) -> Type:
if isinstance(self.s, TypedDictType):
for (name, l, r) in self.s.zip(t):
if (not is_equivalent(l, r) or
(name in t.required_keys) != (name in self.s.required_keys)):
return self.default(self.s)
item_list = [] # type: List[Tuple[str, Type]]
for (item_name, s_item_type, t_item_type) in self.s.zipall(t):
if s_item_type is not None:
item_list.append((item_name, s_item_type))
else:
# at least one of s_item_type and t_item_type is not None
assert t_item_type is not None
item_list.append((item_name, t_item_type))
items = OrderedDict(item_list)
mapping_value_type = join_type_list(list(items.values()))
fallback = self.s.create_anonymous_fallback(value_type=mapping_value_type)
required_keys = t.required_keys | self.s.required_keys
return TypedDictType(items, required_keys, fallback)
else:
return self.default(self.s)
def visit_partial_type(self, t: PartialType) -> Type:
# We can't determine the meet of partial types. We should never get here.
assert False, 'Internal error'
def visit_type_type(self, t: TypeType) -> Type:
if isinstance(self.s, TypeType):
typ = self.meet(t.item, self.s.item)
if not isinstance(typ, NoneTyp):
typ = TypeType.make_normalized(typ, line=t.line)
return typ
elif isinstance(self.s, Instance) and self.s.type.fullname() == 'builtins.type':
return t
else:
return self.default(self.s)
def meet(self, s: Type, t: Type) -> Type:
return meet_types(s, t)
def default(self, typ: Type) -> Type:
if isinstance(typ, UnboundType):
return AnyType()
else:
if experiments.STRICT_OPTIONAL:
return UninhabitedType()
else:
return NoneTyp()
def meet_similar_callables(t: CallableType, s: CallableType) -> CallableType:
from mypy.join import join_types
arg_types = [] # type: List[Type]
for i in range(len(t.arg_types)):
arg_types.append(join_types(t.arg_types[i], s.arg_types[i]))
# TODO in combine_similar_callables also applies here (names and kinds)
# The fallback type can be either 'function' or 'type'. The result should have 'function' as
# fallback only if both operands have it as 'function'.
if t.fallback.type.fullname() != 'builtins.function':
fallback = t.fallback
else:
fallback = s.fallback
return t.copy_modified(arg_types=arg_types,
ret_type=meet_types(t.ret_type, s.ret_type),
fallback=fallback,
name=None)