1714 lines
78 KiB
Python
1714 lines
78 KiB
Python
"""Type inference constraints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable, Sequence
|
|
from typing import TYPE_CHECKING, Final, cast
|
|
from typing_extensions import TypeGuard
|
|
|
|
import mypy.subtypes
|
|
import mypy.typeops
|
|
from mypy.argmap import ArgTypeExpander
|
|
from mypy.erasetype import erase_typevars
|
|
from mypy.maptype import map_instance_to_supertype
|
|
from mypy.nodes import (
|
|
ARG_OPT,
|
|
ARG_POS,
|
|
ARG_STAR,
|
|
ARG_STAR2,
|
|
CONTRAVARIANT,
|
|
COVARIANT,
|
|
ArgKind,
|
|
TypeInfo,
|
|
)
|
|
from mypy.type_visitor import ALL_STRATEGY, BoolTypeQuery
|
|
from mypy.types import (
|
|
TUPLE_LIKE_INSTANCE_NAMES,
|
|
AnyType,
|
|
CallableType,
|
|
DeletedType,
|
|
ErasedType,
|
|
Instance,
|
|
LiteralType,
|
|
NoneType,
|
|
NormalizedCallableType,
|
|
Overloaded,
|
|
Parameters,
|
|
ParamSpecType,
|
|
PartialType,
|
|
ProperType,
|
|
TupleType,
|
|
Type,
|
|
TypeAliasType,
|
|
TypedDictType,
|
|
TypeOfAny,
|
|
TypeType,
|
|
TypeVarId,
|
|
TypeVarLikeType,
|
|
TypeVarTupleType,
|
|
TypeVarType,
|
|
TypeVisitor,
|
|
UnboundType,
|
|
UninhabitedType,
|
|
UnionType,
|
|
UnpackType,
|
|
find_unpack_in_list,
|
|
flatten_nested_tuples,
|
|
get_proper_type,
|
|
has_recursive_types,
|
|
has_type_vars,
|
|
is_named_instance,
|
|
split_with_prefix_and_suffix,
|
|
)
|
|
from mypy.types_utils import is_union_with_any
|
|
from mypy.typestate import type_state
|
|
|
|
if TYPE_CHECKING:
|
|
from mypy.infer import ArgumentInferContext
|
|
|
|
SUBTYPE_OF: Final = 0
|
|
SUPERTYPE_OF: Final = 1
|
|
|
|
|
|
class Constraint:
|
|
"""A representation of a type constraint.
|
|
|
|
It can be either T <: type or T :> type (T is a type variable).
|
|
"""
|
|
|
|
type_var: TypeVarId
|
|
op = 0 # SUBTYPE_OF or SUPERTYPE_OF
|
|
target: Type
|
|
|
|
def __init__(self, type_var: TypeVarLikeType, op: int, target: Type) -> None:
|
|
self.type_var = type_var.id
|
|
self.op = op
|
|
# TODO: should we add "assert not isinstance(target, UnpackType)"?
|
|
# UnpackType is a synthetic type, and is never valid as a constraint target.
|
|
self.target = target
|
|
self.origin_type_var = type_var
|
|
# These are additional type variables that should be solved for together with type_var.
|
|
# TODO: A cleaner solution may be to modify the return type of infer_constraints()
|
|
# to include these instead, but this is a rather big refactoring.
|
|
self.extra_tvars: list[TypeVarLikeType] = []
|
|
|
|
def __repr__(self) -> str:
|
|
op_str = "<:"
|
|
if self.op == SUPERTYPE_OF:
|
|
op_str = ":>"
|
|
return f"{self.type_var} {op_str} {self.target}"
|
|
|
|
def __hash__(self) -> int:
|
|
return hash((self.type_var, self.op, self.target))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, Constraint):
|
|
return False
|
|
return (self.type_var, self.op, self.target) == (other.type_var, other.op, other.target)
|
|
|
|
|
|
def infer_constraints_for_callable(
|
|
callee: CallableType,
|
|
arg_types: Sequence[Type | None],
|
|
arg_kinds: list[ArgKind],
|
|
arg_names: Sequence[str | None] | None,
|
|
formal_to_actual: list[list[int]],
|
|
context: ArgumentInferContext,
|
|
) -> list[Constraint]:
|
|
"""Infer type variable constraints for a callable and actual arguments.
|
|
|
|
Return a list of constraints.
|
|
"""
|
|
constraints: list[Constraint] = []
|
|
mapper = ArgTypeExpander(context)
|
|
|
|
param_spec = callee.param_spec()
|
|
param_spec_arg_types = []
|
|
param_spec_arg_names = []
|
|
param_spec_arg_kinds = []
|
|
|
|
incomplete_star_mapping = False
|
|
for i, actuals in enumerate(formal_to_actual): # TODO: isn't this `enumerate(arg_types)`?
|
|
for actual in actuals:
|
|
if actual is None and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2): # type: ignore[unreachable]
|
|
# We can't use arguments to infer ParamSpec constraint, if only some
|
|
# are present in the current inference pass.
|
|
incomplete_star_mapping = True # type: ignore[unreachable]
|
|
break
|
|
|
|
for i, actuals in enumerate(formal_to_actual):
|
|
if isinstance(callee.arg_types[i], UnpackType):
|
|
unpack_type = callee.arg_types[i]
|
|
assert isinstance(unpack_type, UnpackType)
|
|
|
|
# In this case we are binding all the actuals to *args,
|
|
# and we want a constraint that the typevar tuple being unpacked
|
|
# is equal to a type list of all the actuals.
|
|
actual_types = []
|
|
|
|
unpacked_type = get_proper_type(unpack_type.type)
|
|
if isinstance(unpacked_type, TypeVarTupleType):
|
|
tuple_instance = unpacked_type.tuple_fallback
|
|
elif isinstance(unpacked_type, TupleType):
|
|
tuple_instance = unpacked_type.partial_fallback
|
|
else:
|
|
assert False, "mypy bug: unhandled constraint inference case"
|
|
|
|
for actual in actuals:
|
|
actual_arg_type = arg_types[actual]
|
|
if actual_arg_type is None:
|
|
continue
|
|
|
|
expanded_actual = mapper.expand_actual_type(
|
|
actual_arg_type,
|
|
arg_kinds[actual],
|
|
callee.arg_names[i],
|
|
callee.arg_kinds[i],
|
|
allow_unpack=True,
|
|
)
|
|
|
|
if arg_kinds[actual] != ARG_STAR or isinstance(
|
|
get_proper_type(actual_arg_type), TupleType
|
|
):
|
|
actual_types.append(expanded_actual)
|
|
else:
|
|
# If we are expanding an iterable inside * actual, append a homogeneous item instead
|
|
actual_types.append(
|
|
UnpackType(tuple_instance.copy_modified(args=[expanded_actual]))
|
|
)
|
|
|
|
if isinstance(unpacked_type, TypeVarTupleType):
|
|
constraints.append(
|
|
Constraint(
|
|
unpacked_type,
|
|
SUPERTYPE_OF,
|
|
TupleType(actual_types, unpacked_type.tuple_fallback),
|
|
)
|
|
)
|
|
elif isinstance(unpacked_type, TupleType):
|
|
# Prefixes get converted to positional args, so technically the only case we
|
|
# should have here is like Tuple[Unpack[Ts], Y1, Y2, Y3]. If this turns out
|
|
# not to hold we can always handle the prefixes too.
|
|
inner_unpack = unpacked_type.items[0]
|
|
assert isinstance(inner_unpack, UnpackType)
|
|
inner_unpacked_type = get_proper_type(inner_unpack.type)
|
|
suffix_len = len(unpacked_type.items) - 1
|
|
if isinstance(inner_unpacked_type, TypeVarTupleType):
|
|
# Variadic item can be either *Ts...
|
|
constraints.append(
|
|
Constraint(
|
|
inner_unpacked_type,
|
|
SUPERTYPE_OF,
|
|
TupleType(
|
|
actual_types[:-suffix_len], inner_unpacked_type.tuple_fallback
|
|
),
|
|
)
|
|
)
|
|
else:
|
|
# ...or it can be a homogeneous tuple.
|
|
assert (
|
|
isinstance(inner_unpacked_type, Instance)
|
|
and inner_unpacked_type.type.fullname == "builtins.tuple"
|
|
)
|
|
for at in actual_types[:-suffix_len]:
|
|
constraints.extend(
|
|
infer_constraints(inner_unpacked_type.args[0], at, SUPERTYPE_OF)
|
|
)
|
|
# Now handle the suffix (if any).
|
|
if suffix_len:
|
|
for tt, at in zip(unpacked_type.items[1:], actual_types[-suffix_len:]):
|
|
constraints.extend(infer_constraints(tt, at, SUPERTYPE_OF))
|
|
else:
|
|
assert False, "mypy bug: unhandled constraint inference case"
|
|
else:
|
|
for actual in actuals:
|
|
actual_arg_type = arg_types[actual]
|
|
if actual_arg_type is None:
|
|
continue
|
|
|
|
if param_spec and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2):
|
|
# If actual arguments are mapped to ParamSpec type, we can't infer individual
|
|
# constraints, instead store them and infer single constraint at the end.
|
|
# It is impossible to map actual kind to formal kind, so use some heuristic.
|
|
# This inference is used as a fallback, so relying on heuristic should be OK.
|
|
if not incomplete_star_mapping:
|
|
param_spec_arg_types.append(
|
|
mapper.expand_actual_type(
|
|
actual_arg_type, arg_kinds[actual], None, arg_kinds[actual]
|
|
)
|
|
)
|
|
actual_kind = arg_kinds[actual]
|
|
param_spec_arg_kinds.append(
|
|
ARG_POS if actual_kind not in (ARG_STAR, ARG_STAR2) else actual_kind
|
|
)
|
|
param_spec_arg_names.append(arg_names[actual] if arg_names else None)
|
|
else:
|
|
actual_type = mapper.expand_actual_type(
|
|
actual_arg_type,
|
|
arg_kinds[actual],
|
|
callee.arg_names[i],
|
|
callee.arg_kinds[i],
|
|
)
|
|
c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF)
|
|
constraints.extend(c)
|
|
if (
|
|
param_spec
|
|
and not any(c.type_var == param_spec.id for c in constraints)
|
|
and not incomplete_star_mapping
|
|
):
|
|
# Use ParamSpec constraint from arguments only if there are no other constraints,
|
|
# since as explained above it is quite ad-hoc.
|
|
constraints.append(
|
|
Constraint(
|
|
param_spec,
|
|
SUPERTYPE_OF,
|
|
Parameters(
|
|
arg_types=param_spec_arg_types,
|
|
arg_kinds=param_spec_arg_kinds,
|
|
arg_names=param_spec_arg_names,
|
|
imprecise_arg_kinds=True,
|
|
),
|
|
)
|
|
)
|
|
if any(isinstance(v, ParamSpecType) for v in callee.variables):
|
|
# As a perf optimization filter imprecise constraints only when we can have them.
|
|
constraints = filter_imprecise_kinds(constraints)
|
|
return constraints
|
|
|
|
|
|
def infer_constraints(
|
|
template: Type, actual: Type, direction: int, skip_neg_op: bool = False
|
|
) -> list[Constraint]:
|
|
"""Infer type constraints.
|
|
|
|
Match a template type, which may contain type variable references,
|
|
recursively against a type which does not contain (the same) type
|
|
variable references. The result is a list of type constrains of
|
|
form 'T is a supertype/subtype of x', where T is a type variable
|
|
present in the template and x is a type without reference to type
|
|
variables present in the template.
|
|
|
|
Assume T and S are type variables. Now the following results can be
|
|
calculated (read as '(template, actual) --> result'):
|
|
|
|
(T, X) --> T :> X
|
|
(X[T], X[Y]) --> T <: Y and T :> Y
|
|
((T, T), (X, Y)) --> T :> X and T :> Y
|
|
((T, S), (X, Y)) --> T :> X and S :> Y
|
|
(X[T], Any) --> T <: Any and T :> Any
|
|
|
|
The constraints are represented as Constraint objects. If skip_neg_op == True,
|
|
then skip adding reverse (polymorphic) constraints (since this is already a call
|
|
to infer such constraints).
|
|
"""
|
|
if any(
|
|
get_proper_type(template) == get_proper_type(t)
|
|
and get_proper_type(actual) == get_proper_type(a)
|
|
for (t, a) in reversed(type_state.inferring)
|
|
):
|
|
return []
|
|
if has_recursive_types(template) or isinstance(get_proper_type(template), Instance):
|
|
# This case requires special care because it may cause infinite recursion.
|
|
# Note that we include Instances because the may be recursive as str(Sequence[str]).
|
|
if not has_type_vars(template):
|
|
# Return early on an empty branch.
|
|
return []
|
|
type_state.inferring.append((template, actual))
|
|
res = _infer_constraints(template, actual, direction, skip_neg_op)
|
|
type_state.inferring.pop()
|
|
return res
|
|
return _infer_constraints(template, actual, direction, skip_neg_op)
|
|
|
|
|
|
def _infer_constraints(
|
|
template: Type, actual: Type, direction: int, skip_neg_op: bool
|
|
) -> list[Constraint]:
|
|
orig_template = template
|
|
template = get_proper_type(template)
|
|
actual = get_proper_type(actual)
|
|
|
|
# Type inference shouldn't be affected by whether union types have been simplified.
|
|
# We however keep any ErasedType items, so that the caller will see it when using
|
|
# checkexpr.has_erased_component().
|
|
if isinstance(template, UnionType):
|
|
template = mypy.typeops.make_simplified_union(template.items, keep_erased=True)
|
|
if isinstance(actual, UnionType):
|
|
actual = mypy.typeops.make_simplified_union(actual.items, keep_erased=True)
|
|
|
|
# Ignore Any types from the type suggestion engine to avoid them
|
|
# causing us to infer Any in situations where a better job could
|
|
# be done otherwise. (This can produce false positives but that
|
|
# doesn't really matter because it is all heuristic anyway.)
|
|
if isinstance(actual, AnyType) and actual.type_of_any == TypeOfAny.suggestion_engine:
|
|
return []
|
|
|
|
# type[A | B] is always represented as type[A] | type[B] internally.
|
|
# This makes our constraint solver choke on type[T] <: type[A] | type[B],
|
|
# solving T as generic meet(A, B) which is often `object`. Force unwrap such unions
|
|
# if both sides are type[...] or unions thereof. See `testTypeVarType` test
|
|
type_type_unwrapped = False
|
|
if _is_type_type(template) and _is_type_type(actual):
|
|
type_type_unwrapped = True
|
|
template = _unwrap_type_type(template)
|
|
actual = _unwrap_type_type(actual)
|
|
|
|
# If the template is simply a type variable, emit a Constraint directly.
|
|
# We need to handle this case before handling Unions for two reasons:
|
|
# 1. "T <: Union[U1, U2]" is not equivalent to "T <: U1 or T <: U2",
|
|
# because T can itself be a union (notably, Union[U1, U2] itself).
|
|
# 2. "T :> Union[U1, U2]" is logically equivalent to "T :> U1 and
|
|
# T :> U2", but they are not equivalent to the constraint solver,
|
|
# which never introduces new Union types (it uses join() instead).
|
|
if isinstance(template, TypeVarType):
|
|
return [Constraint(template, direction, actual)]
|
|
|
|
if (
|
|
isinstance(actual, TypeVarType)
|
|
and not actual.id.is_meta_var()
|
|
and direction == SUPERTYPE_OF
|
|
):
|
|
# Unless template is also a type variable (or a union that contains one), using the upper
|
|
# bound for inference will usually give better result for actual that is a type variable.
|
|
if not isinstance(template, UnionType) or not any(
|
|
isinstance(t, TypeVarType) for t in template.items
|
|
):
|
|
actual = get_proper_type(actual.upper_bound)
|
|
|
|
# Now handle the case of either template or actual being a Union.
|
|
# For a Union to be a subtype of another type, every item of the Union
|
|
# must be a subtype of that type, so concatenate the constraints.
|
|
if direction == SUBTYPE_OF and isinstance(template, UnionType):
|
|
res = []
|
|
for t_item in template.items:
|
|
res.extend(infer_constraints(t_item, actual, direction))
|
|
return res
|
|
if direction == SUPERTYPE_OF and isinstance(actual, UnionType):
|
|
res = []
|
|
for a_item in actual.items:
|
|
# `orig_template` has to be preserved intact in case it's recursive.
|
|
# If we unwrapped ``type[...]`` previously, wrap the item back again,
|
|
# as ``type[...]`` can't be removed from `orig_template`.
|
|
if type_type_unwrapped:
|
|
a_item = TypeType.make_normalized(a_item)
|
|
res.extend(infer_constraints(orig_template, a_item, direction))
|
|
return res
|
|
|
|
# Now the potential subtype is known not to be a Union or a type
|
|
# variable that we are solving for. In that case, for a Union to
|
|
# be a supertype of the potential subtype, some item of the Union
|
|
# must be a supertype of it.
|
|
if direction == SUBTYPE_OF and isinstance(actual, UnionType):
|
|
# If some of items is not a complete type, disregard that.
|
|
items = simplify_away_incomplete_types(actual.items)
|
|
# We infer constraints eagerly -- try to find constraints for a type
|
|
# variable if possible. This seems to help with some real-world
|
|
# use cases.
|
|
return any_constraints(
|
|
[infer_constraints_if_possible(template, a_item, direction) for a_item in items],
|
|
eager=True,
|
|
)
|
|
if direction == SUPERTYPE_OF and isinstance(template, UnionType):
|
|
# When the template is a union, we are okay with leaving some
|
|
# type variables indeterminate. This helps with some special
|
|
# cases, though this isn't very principled.
|
|
result = any_constraints(
|
|
[
|
|
infer_constraints_if_possible(t_item, actual, direction)
|
|
for t_item in template.items
|
|
],
|
|
eager=isinstance(actual, AnyType),
|
|
)
|
|
if result:
|
|
return result
|
|
elif has_recursive_types(template) and not has_recursive_types(actual):
|
|
return handle_recursive_union(template, actual, direction)
|
|
return []
|
|
|
|
# Remaining cases are handled by ConstraintBuilderVisitor.
|
|
return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op))
|
|
|
|
|
|
def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]:
|
|
"""Is ``tp`` a ``type[...]`` or a union thereof?
|
|
|
|
``Type[A | B]`` is internally represented as ``type[A] | type[B]``, and this
|
|
troubles the solver sometimes.
|
|
"""
|
|
return (
|
|
isinstance(tp, TypeType)
|
|
or isinstance(tp, UnionType)
|
|
and all(isinstance(get_proper_type(o), TypeType) for o in tp.items)
|
|
)
|
|
|
|
|
|
def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType:
|
|
"""Extract the inner type from ``type[...]`` expression or a union thereof."""
|
|
if isinstance(tp, TypeType):
|
|
return tp.item
|
|
return UnionType.make_union([cast(TypeType, get_proper_type(o)).item for o in tp.items])
|
|
|
|
|
|
def infer_constraints_if_possible(
|
|
template: Type, actual: Type, direction: int
|
|
) -> list[Constraint] | None:
|
|
"""Like infer_constraints, but return None if the input relation is
|
|
known to be unsatisfiable, for example if template=List[T] and actual=int.
|
|
(In this case infer_constraints would return [], just like it would for
|
|
an automatically satisfied relation like template=List[T] and actual=object.)
|
|
"""
|
|
if direction == SUBTYPE_OF and not mypy.subtypes.is_subtype(erase_typevars(template), actual):
|
|
return None
|
|
if direction == SUPERTYPE_OF and not mypy.subtypes.is_subtype(
|
|
actual, erase_typevars(template)
|
|
):
|
|
return None
|
|
if (
|
|
direction == SUPERTYPE_OF
|
|
and isinstance(template, TypeVarType)
|
|
and not mypy.subtypes.is_subtype(actual, erase_typevars(template.upper_bound))
|
|
):
|
|
# This is not caught by the above branch because of the erase_typevars() call,
|
|
# that would return 'Any' for a type variable.
|
|
return None
|
|
return infer_constraints(template, actual, direction)
|
|
|
|
|
|
def select_trivial(options: Sequence[list[Constraint] | None]) -> list[list[Constraint]]:
|
|
"""Select only those lists where each item is a constraint against Any."""
|
|
res = []
|
|
for option in options:
|
|
if option is None:
|
|
continue
|
|
if all(isinstance(get_proper_type(c.target), AnyType) for c in option):
|
|
res.append(option)
|
|
return res
|
|
|
|
|
|
def merge_with_any(constraint: Constraint) -> Constraint:
|
|
"""Transform a constraint target into a union with given Any type."""
|
|
target = constraint.target
|
|
if is_union_with_any(target):
|
|
# Do not produce redundant unions.
|
|
return constraint
|
|
# TODO: if we will support multiple sources Any, use this here instead.
|
|
any_type = AnyType(TypeOfAny.implementation_artifact)
|
|
return Constraint(
|
|
constraint.origin_type_var,
|
|
constraint.op,
|
|
UnionType.make_union([target, any_type], target.line, target.column),
|
|
)
|
|
|
|
|
|
def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> list[Constraint]:
|
|
# This is a hack to special-case things like Union[T, Inst[T]] in recursive types. Although
|
|
# it is quite arbitrary, it is a relatively common pattern, so we should handle it well.
|
|
# This function may be called when inferring against such union resulted in different
|
|
# constraints for each item. Normally we give up in such case, but here we instead split
|
|
# the union in two parts, and try inferring sequentially.
|
|
non_type_var_items = [t for t in template.items if not isinstance(t, TypeVarType)]
|
|
type_var_items = [t for t in template.items if isinstance(t, TypeVarType)]
|
|
return infer_constraints(
|
|
UnionType.make_union(non_type_var_items), actual, direction
|
|
) or infer_constraints(UnionType.make_union(type_var_items), actual, direction)
|
|
|
|
|
|
def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> list[Constraint]:
|
|
"""Deduce what we can from a collection of constraint lists.
|
|
|
|
It's a given that at least one of the lists must be satisfied. A
|
|
None element in the list of options represents an unsatisfiable
|
|
constraint and is ignored. Ignore empty constraint lists if eager
|
|
is true -- they are always trivially satisfiable.
|
|
"""
|
|
if eager:
|
|
valid_options = [option for option in options if option]
|
|
else:
|
|
valid_options = [option for option in options if option is not None]
|
|
|
|
if not valid_options:
|
|
return []
|
|
|
|
if len(valid_options) == 1:
|
|
return valid_options[0]
|
|
|
|
if all(is_same_constraints(valid_options[0], c) for c in valid_options[1:]):
|
|
# Multiple sets of constraints that are all the same. Just pick any one of them.
|
|
return valid_options[0]
|
|
|
|
if all(is_similar_constraints(valid_options[0], c) for c in valid_options[1:]):
|
|
# All options have same structure. In this case we can merge-in trivial
|
|
# options (i.e. those that only have Any) and try again.
|
|
# TODO: More generally, if a given (variable, direction) pair appears in
|
|
# every option, combine the bounds with meet/join always, not just for Any.
|
|
trivial_options = select_trivial(valid_options)
|
|
if trivial_options and len(trivial_options) < len(valid_options):
|
|
merged_options = []
|
|
for option in valid_options:
|
|
if option in trivial_options:
|
|
continue
|
|
merged_options.append([merge_with_any(c) for c in option])
|
|
return any_constraints(list(merged_options), eager=eager)
|
|
|
|
# If normal logic didn't work, try excluding trivially unsatisfiable constraint (due to
|
|
# upper bounds) from each option, and comparing them again.
|
|
filtered_options = [filter_satisfiable(o) for o in options]
|
|
if filtered_options != options:
|
|
return any_constraints(filtered_options, eager=eager)
|
|
|
|
# Try harder: if that didn't work, try to strip typevars that aren't meta vars.
|
|
# Note this is what we would always do, but unfortunately some callers may not
|
|
# set the meta var status correctly (for historical reasons), so we use this as
|
|
# a fallback only.
|
|
filtered_options = [exclude_non_meta_vars(o) for o in options]
|
|
if filtered_options != options:
|
|
return any_constraints(filtered_options, eager=eager)
|
|
|
|
# Otherwise, there are either no valid options or multiple, inconsistent valid
|
|
# options. Give up and deduce nothing.
|
|
return []
|
|
|
|
|
|
def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | None:
|
|
"""Keep only constraints that can possibly be satisfied.
|
|
|
|
Currently, we filter out constraints where target is not a subtype of the upper bound.
|
|
Since those can be never satisfied. We may add more cases in future if it improves type
|
|
inference.
|
|
"""
|
|
if not option:
|
|
return option
|
|
|
|
satisfiable = []
|
|
for c in option:
|
|
if isinstance(c.origin_type_var, TypeVarType) and c.origin_type_var.values:
|
|
if any(
|
|
mypy.subtypes.is_subtype(c.target, value) for value in c.origin_type_var.values
|
|
):
|
|
satisfiable.append(c)
|
|
elif mypy.subtypes.is_subtype(c.target, c.origin_type_var.upper_bound):
|
|
satisfiable.append(c)
|
|
if not satisfiable:
|
|
return None
|
|
return satisfiable
|
|
|
|
|
|
def exclude_non_meta_vars(option: list[Constraint] | None) -> list[Constraint] | None:
|
|
# If we had an empty list, keep it intact
|
|
if not option:
|
|
return option
|
|
# However, if none of the options actually references meta vars, better remove
|
|
# this constraint entirely.
|
|
return [c for c in option if c.type_var.is_meta_var()] or None
|
|
|
|
|
|
def is_same_constraints(x: list[Constraint], y: list[Constraint]) -> bool:
|
|
for c1 in x:
|
|
if not any(is_same_constraint(c1, c2) for c2 in y):
|
|
return False
|
|
for c1 in y:
|
|
if not any(is_same_constraint(c1, c2) for c2 in x):
|
|
return False
|
|
return True
|
|
|
|
|
|
def is_same_constraint(c1: Constraint, c2: Constraint) -> bool:
|
|
# Ignore direction when comparing constraints against Any.
|
|
skip_op_check = isinstance(get_proper_type(c1.target), AnyType) and isinstance(
|
|
get_proper_type(c2.target), AnyType
|
|
)
|
|
return (
|
|
c1.type_var == c2.type_var
|
|
and (c1.op == c2.op or skip_op_check)
|
|
and mypy.subtypes.is_same_type(c1.target, c2.target)
|
|
)
|
|
|
|
|
|
def is_similar_constraints(x: list[Constraint], y: list[Constraint]) -> bool:
|
|
"""Check that two lists of constraints have similar structure.
|
|
|
|
This means that each list has same type variable plus direction pairs (i.e we
|
|
ignore the target). Except for constraints where target is Any type, there
|
|
we ignore direction as well.
|
|
"""
|
|
return _is_similar_constraints(x, y) and _is_similar_constraints(y, x)
|
|
|
|
|
|
def _is_similar_constraints(x: list[Constraint], y: list[Constraint]) -> bool:
|
|
"""Check that every constraint in the first list has a similar one in the second.
|
|
|
|
See docstring above for definition of similarity.
|
|
"""
|
|
for c1 in x:
|
|
has_similar = False
|
|
for c2 in y:
|
|
# Ignore direction when either constraint is against Any.
|
|
skip_op_check = isinstance(get_proper_type(c1.target), AnyType) or isinstance(
|
|
get_proper_type(c2.target), AnyType
|
|
)
|
|
if c1.type_var == c2.type_var and (c1.op == c2.op or skip_op_check):
|
|
has_similar = True
|
|
break
|
|
if not has_similar:
|
|
return False
|
|
return True
|
|
|
|
|
|
def simplify_away_incomplete_types(types: Iterable[Type]) -> list[Type]:
|
|
complete = [typ for typ in types if is_complete_type(typ)]
|
|
if complete:
|
|
return complete
|
|
else:
|
|
return list(types)
|
|
|
|
|
|
def is_complete_type(typ: Type) -> bool:
|
|
"""Is a type complete?
|
|
|
|
A complete doesn't have uninhabited type components or (when not in strict
|
|
optional mode) None components.
|
|
"""
|
|
return typ.accept(CompleteTypeVisitor())
|
|
|
|
|
|
class CompleteTypeVisitor(BoolTypeQuery):
|
|
def __init__(self) -> None:
|
|
super().__init__(ALL_STRATEGY)
|
|
|
|
def visit_uninhabited_type(self, t: UninhabitedType) -> bool:
|
|
return False
|
|
|
|
|
|
class ConstraintBuilderVisitor(TypeVisitor[list[Constraint]]):
|
|
"""Visitor class for inferring type constraints."""
|
|
|
|
# The type that is compared against a template
|
|
# TODO: The value may be None. Is that actually correct?
|
|
actual: ProperType
|
|
|
|
def __init__(self, actual: ProperType, direction: int, skip_neg_op: bool) -> None:
|
|
# Direction must be SUBTYPE_OF or SUPERTYPE_OF.
|
|
self.actual = actual
|
|
self.direction = direction
|
|
# Whether to skip polymorphic inference (involves inference in opposite direction)
|
|
# this is used to prevent infinite recursion when both template and actual are
|
|
# generic callables.
|
|
self.skip_neg_op = skip_neg_op
|
|
|
|
# Trivial leaf types
|
|
|
|
def visit_unbound_type(self, template: UnboundType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_any(self, template: AnyType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_none_type(self, template: NoneType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_uninhabited_type(self, template: UninhabitedType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_erased_type(self, template: ErasedType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_deleted_type(self, template: DeletedType) -> list[Constraint]:
|
|
return []
|
|
|
|
def visit_literal_type(self, template: LiteralType) -> list[Constraint]:
|
|
return []
|
|
|
|
# Errors
|
|
|
|
def visit_partial_type(self, template: PartialType) -> list[Constraint]:
|
|
# We can't do anything useful with a partial type here.
|
|
assert False, "Internal error"
|
|
|
|
# Non-trivial leaf type
|
|
|
|
def visit_type_var(self, template: TypeVarType) -> list[Constraint]:
|
|
assert False, (
|
|
"Unexpected TypeVarType in ConstraintBuilderVisitor"
|
|
" (should have been handled in infer_constraints)"
|
|
)
|
|
|
|
def visit_param_spec(self, template: ParamSpecType) -> list[Constraint]:
|
|
# Can't infer ParamSpecs from component values (only via Callable[P, T]).
|
|
return []
|
|
|
|
def visit_type_var_tuple(self, template: TypeVarTupleType) -> list[Constraint]:
|
|
raise NotImplementedError
|
|
|
|
def visit_unpack_type(self, template: UnpackType) -> list[Constraint]:
|
|
raise RuntimeError("Mypy bug: unpack should be handled at a higher level.")
|
|
|
|
def visit_parameters(self, template: Parameters) -> list[Constraint]:
|
|
# Constraining Any against C[P] turns into infer_against_any([P], Any)
|
|
if isinstance(self.actual, AnyType):
|
|
return self.infer_against_any(template.arg_types, self.actual)
|
|
if type_state.infer_polymorphic and isinstance(self.actual, Parameters):
|
|
# For polymorphic inference we need to be able to infer secondary constraints
|
|
# in situations like [x: T] <: P <: [x: int].
|
|
return infer_callable_arguments_constraints(template, self.actual, self.direction)
|
|
if type_state.infer_polymorphic and isinstance(self.actual, ParamSpecType):
|
|
# Similar for [x: T] <: Q <: Concatenate[int, P].
|
|
return infer_callable_arguments_constraints(
|
|
template, self.actual.prefix, self.direction
|
|
)
|
|
# There also may be unpatched types after a user error, simply ignore them.
|
|
return []
|
|
|
|
# Non-leaf types
|
|
|
|
def visit_instance(self, template: Instance) -> list[Constraint]:
|
|
original_actual = actual = self.actual
|
|
res: list[Constraint] = []
|
|
if isinstance(actual, (CallableType, Overloaded)) and template.type.is_protocol:
|
|
if "__call__" in template.type.protocol_members:
|
|
# Special case: a generic callback protocol
|
|
if not any(template == t for t in template.type.inferring):
|
|
template.type.inferring.append(template)
|
|
call = mypy.subtypes.find_member(
|
|
"__call__", template, actual, is_operator=True
|
|
)
|
|
assert call is not None
|
|
if (
|
|
self.direction == SUPERTYPE_OF
|
|
and mypy.subtypes.is_subtype(actual, erase_typevars(call))
|
|
or self.direction == SUBTYPE_OF
|
|
and mypy.subtypes.is_subtype(erase_typevars(call), actual)
|
|
):
|
|
res.extend(infer_constraints(call, actual, self.direction))
|
|
template.type.inferring.pop()
|
|
if isinstance(actual, CallableType) and actual.fallback is not None:
|
|
if (
|
|
actual.is_type_obj()
|
|
and template.type.is_protocol
|
|
and self.direction == SUPERTYPE_OF
|
|
):
|
|
ret_type = get_proper_type(actual.ret_type)
|
|
if isinstance(ret_type, TupleType):
|
|
ret_type = mypy.typeops.tuple_fallback(ret_type)
|
|
if isinstance(ret_type, Instance):
|
|
res.extend(
|
|
self.infer_constraints_from_protocol_members(
|
|
ret_type, template, ret_type, template, class_obj=True
|
|
)
|
|
)
|
|
actual = actual.fallback
|
|
if isinstance(actual, TypeType) and template.type.is_protocol:
|
|
if self.direction == SUPERTYPE_OF:
|
|
a_item = actual.item
|
|
if isinstance(a_item, Instance):
|
|
res.extend(
|
|
self.infer_constraints_from_protocol_members(
|
|
a_item, template, a_item, template, class_obj=True
|
|
)
|
|
)
|
|
# Infer constraints for Type[T] via metaclass of T when it makes sense.
|
|
if isinstance(a_item, TypeVarType):
|
|
a_item = get_proper_type(a_item.upper_bound)
|
|
if isinstance(a_item, Instance) and a_item.type.metaclass_type:
|
|
res.extend(
|
|
self.infer_constraints_from_protocol_members(
|
|
a_item.type.metaclass_type, template, actual, template
|
|
)
|
|
)
|
|
|
|
if isinstance(actual, Overloaded) and actual.fallback is not None:
|
|
actual = actual.fallback
|
|
if isinstance(actual, TypedDictType):
|
|
actual = actual.as_anonymous().fallback
|
|
if isinstance(actual, LiteralType):
|
|
actual = actual.fallback
|
|
if isinstance(actual, Instance):
|
|
instance = actual
|
|
erased = erase_typevars(template)
|
|
assert isinstance(erased, Instance) # type: ignore[misc]
|
|
# We always try nominal inference if possible,
|
|
# it is much faster than the structural one.
|
|
if self.direction == SUBTYPE_OF and template.type.has_base(instance.type.fullname):
|
|
mapped = map_instance_to_supertype(template, instance.type)
|
|
tvars = mapped.type.defn.type_vars
|
|
|
|
if instance.type.has_type_var_tuple_type:
|
|
# Variadic types need special handling to map each type argument to
|
|
# the correct corresponding type variable.
|
|
assert instance.type.type_var_tuple_prefix is not None
|
|
assert instance.type.type_var_tuple_suffix is not None
|
|
prefix_len = instance.type.type_var_tuple_prefix
|
|
suffix_len = instance.type.type_var_tuple_suffix
|
|
tvt = instance.type.defn.type_vars[prefix_len]
|
|
assert isinstance(tvt, TypeVarTupleType)
|
|
fallback = tvt.tuple_fallback
|
|
i_prefix, i_middle, i_suffix = split_with_prefix_and_suffix(
|
|
instance.args, prefix_len, suffix_len
|
|
)
|
|
m_prefix, m_middle, m_suffix = split_with_prefix_and_suffix(
|
|
mapped.args, prefix_len, suffix_len
|
|
)
|
|
instance_args = i_prefix + (TupleType(list(i_middle), fallback),) + i_suffix
|
|
mapped_args = m_prefix + (TupleType(list(m_middle), fallback),) + m_suffix
|
|
else:
|
|
mapped_args = mapped.args
|
|
instance_args = instance.args
|
|
|
|
# N.B: We use zip instead of indexing because the lengths might have
|
|
# mismatches during daemon reprocessing.
|
|
for tvar, mapped_arg, instance_arg in zip(tvars, mapped_args, instance_args):
|
|
if isinstance(tvar, TypeVarType):
|
|
# The constraints for generic type parameters depend on variance.
|
|
# Include constraints from both directions if invariant.
|
|
if tvar.variance != CONTRAVARIANT:
|
|
res.extend(infer_constraints(mapped_arg, instance_arg, self.direction))
|
|
if tvar.variance != COVARIANT:
|
|
res.extend(
|
|
infer_constraints(mapped_arg, instance_arg, neg_op(self.direction))
|
|
)
|
|
elif isinstance(tvar, ParamSpecType) and isinstance(mapped_arg, ParamSpecType):
|
|
prefix = mapped_arg.prefix
|
|
if isinstance(instance_arg, Parameters):
|
|
# No such thing as variance for ParamSpecs, consider them invariant
|
|
# TODO: constraints between prefixes using
|
|
# infer_callable_arguments_constraints()
|
|
suffix: Type = instance_arg.copy_modified(
|
|
instance_arg.arg_types[len(prefix.arg_types) :],
|
|
instance_arg.arg_kinds[len(prefix.arg_kinds) :],
|
|
instance_arg.arg_names[len(prefix.arg_names) :],
|
|
)
|
|
res.append(Constraint(mapped_arg, SUBTYPE_OF, suffix))
|
|
res.append(Constraint(mapped_arg, SUPERTYPE_OF, suffix))
|
|
elif isinstance(instance_arg, ParamSpecType):
|
|
suffix = instance_arg.copy_modified(
|
|
prefix=Parameters(
|
|
instance_arg.prefix.arg_types[len(prefix.arg_types) :],
|
|
instance_arg.prefix.arg_kinds[len(prefix.arg_kinds) :],
|
|
instance_arg.prefix.arg_names[len(prefix.arg_names) :],
|
|
)
|
|
)
|
|
res.append(Constraint(mapped_arg, SUBTYPE_OF, suffix))
|
|
res.append(Constraint(mapped_arg, SUPERTYPE_OF, suffix))
|
|
elif isinstance(tvar, TypeVarTupleType):
|
|
# Handle variadic type variables covariantly for consistency.
|
|
res.extend(infer_constraints(mapped_arg, instance_arg, self.direction))
|
|
|
|
return res
|
|
elif self.direction == SUPERTYPE_OF and instance.type.has_base(template.type.fullname):
|
|
mapped = map_instance_to_supertype(instance, template.type)
|
|
tvars = template.type.defn.type_vars
|
|
if template.type.has_type_var_tuple_type:
|
|
# Variadic types need special handling to map each type argument to
|
|
# the correct corresponding type variable.
|
|
assert template.type.type_var_tuple_prefix is not None
|
|
assert template.type.type_var_tuple_suffix is not None
|
|
prefix_len = template.type.type_var_tuple_prefix
|
|
suffix_len = template.type.type_var_tuple_suffix
|
|
tvt = template.type.defn.type_vars[prefix_len]
|
|
assert isinstance(tvt, TypeVarTupleType)
|
|
fallback = tvt.tuple_fallback
|
|
t_prefix, t_middle, t_suffix = split_with_prefix_and_suffix(
|
|
template.args, prefix_len, suffix_len
|
|
)
|
|
m_prefix, m_middle, m_suffix = split_with_prefix_and_suffix(
|
|
mapped.args, prefix_len, suffix_len
|
|
)
|
|
template_args = t_prefix + (TupleType(list(t_middle), fallback),) + t_suffix
|
|
mapped_args = m_prefix + (TupleType(list(m_middle), fallback),) + m_suffix
|
|
else:
|
|
mapped_args = mapped.args
|
|
template_args = template.args
|
|
# N.B: We use zip instead of indexing because the lengths might have
|
|
# mismatches during daemon reprocessing.
|
|
for tvar, mapped_arg, template_arg in zip(tvars, mapped_args, template_args):
|
|
if isinstance(tvar, TypeVarType):
|
|
# The constraints for generic type parameters depend on variance.
|
|
# Include constraints from both directions if invariant.
|
|
if tvar.variance != CONTRAVARIANT:
|
|
res.extend(infer_constraints(template_arg, mapped_arg, self.direction))
|
|
if tvar.variance != COVARIANT:
|
|
res.extend(
|
|
infer_constraints(template_arg, mapped_arg, neg_op(self.direction))
|
|
)
|
|
elif isinstance(tvar, ParamSpecType) and isinstance(
|
|
template_arg, ParamSpecType
|
|
):
|
|
prefix = template_arg.prefix
|
|
if isinstance(mapped_arg, Parameters):
|
|
# No such thing as variance for ParamSpecs, consider them invariant
|
|
# TODO: constraints between prefixes using
|
|
# infer_callable_arguments_constraints()
|
|
suffix = mapped_arg.copy_modified(
|
|
mapped_arg.arg_types[len(prefix.arg_types) :],
|
|
mapped_arg.arg_kinds[len(prefix.arg_kinds) :],
|
|
mapped_arg.arg_names[len(prefix.arg_names) :],
|
|
)
|
|
res.append(Constraint(template_arg, SUBTYPE_OF, suffix))
|
|
res.append(Constraint(template_arg, SUPERTYPE_OF, suffix))
|
|
elif isinstance(mapped_arg, ParamSpecType):
|
|
suffix = mapped_arg.copy_modified(
|
|
prefix=Parameters(
|
|
mapped_arg.prefix.arg_types[len(prefix.arg_types) :],
|
|
mapped_arg.prefix.arg_kinds[len(prefix.arg_kinds) :],
|
|
mapped_arg.prefix.arg_names[len(prefix.arg_names) :],
|
|
)
|
|
)
|
|
res.append(Constraint(template_arg, SUBTYPE_OF, suffix))
|
|
res.append(Constraint(template_arg, SUPERTYPE_OF, suffix))
|
|
elif isinstance(tvar, TypeVarTupleType):
|
|
# Consider variadic type variables to be invariant.
|
|
res.extend(infer_constraints(template_arg, mapped_arg, SUBTYPE_OF))
|
|
res.extend(infer_constraints(template_arg, mapped_arg, SUPERTYPE_OF))
|
|
return res
|
|
if (
|
|
template.type.is_protocol
|
|
and self.direction == SUPERTYPE_OF
|
|
and
|
|
# We avoid infinite recursion for structural subtypes by checking
|
|
# whether this type already appeared in the inference chain.
|
|
# This is a conservative way to break the inference cycles.
|
|
# It never produces any "false" constraints but gives up soon
|
|
# on purely structural inference cycles, see #3829.
|
|
# Note that we use is_protocol_implementation instead of is_subtype
|
|
# because some type may be considered a subtype of a protocol
|
|
# due to _promote, but still not implement the protocol.
|
|
not any(template == t for t in reversed(template.type.inferring))
|
|
and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"])
|
|
):
|
|
template.type.inferring.append(template)
|
|
res.extend(
|
|
self.infer_constraints_from_protocol_members(
|
|
instance, template, original_actual, template
|
|
)
|
|
)
|
|
template.type.inferring.pop()
|
|
return res
|
|
elif (
|
|
instance.type.is_protocol
|
|
and self.direction == SUBTYPE_OF
|
|
and
|
|
# We avoid infinite recursion for structural subtypes also here.
|
|
not any(instance == i for i in reversed(instance.type.inferring))
|
|
and mypy.subtypes.is_protocol_implementation(erased, instance, skip=["__call__"])
|
|
):
|
|
instance.type.inferring.append(instance)
|
|
res.extend(
|
|
self.infer_constraints_from_protocol_members(
|
|
instance, template, template, instance
|
|
)
|
|
)
|
|
instance.type.inferring.pop()
|
|
return res
|
|
if res:
|
|
return res
|
|
|
|
if isinstance(actual, AnyType):
|
|
return self.infer_against_any(template.args, actual)
|
|
if (
|
|
isinstance(actual, TupleType)
|
|
and is_named_instance(template, TUPLE_LIKE_INSTANCE_NAMES)
|
|
and self.direction == SUPERTYPE_OF
|
|
):
|
|
for item in actual.items:
|
|
if isinstance(item, UnpackType):
|
|
unpacked = get_proper_type(item.type)
|
|
if isinstance(unpacked, TypeVarTupleType):
|
|
# Cannot infer anything for T from [T, ...] <: *Ts
|
|
continue
|
|
assert (
|
|
isinstance(unpacked, Instance)
|
|
and unpacked.type.fullname == "builtins.tuple"
|
|
)
|
|
item = unpacked.args[0]
|
|
cb = infer_constraints(template.args[0], item, SUPERTYPE_OF)
|
|
res.extend(cb)
|
|
return res
|
|
elif isinstance(actual, TupleType) and self.direction == SUPERTYPE_OF:
|
|
return infer_constraints(template, mypy.typeops.tuple_fallback(actual), self.direction)
|
|
elif isinstance(actual, TypeVarType):
|
|
if not actual.values and not actual.id.is_meta_var():
|
|
return infer_constraints(template, actual.upper_bound, self.direction)
|
|
return []
|
|
elif isinstance(actual, ParamSpecType):
|
|
return infer_constraints(template, actual.upper_bound, self.direction)
|
|
elif isinstance(actual, TypeVarTupleType):
|
|
raise NotImplementedError
|
|
else:
|
|
return []
|
|
|
|
def infer_constraints_from_protocol_members(
|
|
self,
|
|
instance: Instance,
|
|
template: Instance,
|
|
subtype: Type,
|
|
protocol: Instance,
|
|
class_obj: bool = False,
|
|
) -> list[Constraint]:
|
|
"""Infer constraints for situations where either 'template' or 'instance' is a protocol.
|
|
|
|
The 'protocol' is the one of two that is an instance of protocol type, 'subtype'
|
|
is the type used to bind self during inference. Currently, we just infer constrains for
|
|
every protocol member type (both ways for settable members).
|
|
"""
|
|
res = []
|
|
for member in protocol.type.protocol_members:
|
|
inst = mypy.subtypes.find_member(member, instance, subtype, class_obj=class_obj)
|
|
temp = mypy.subtypes.find_member(member, template, subtype)
|
|
if inst is None or temp is None:
|
|
if member == "__call__":
|
|
continue
|
|
return [] # See #11020
|
|
# The above is safe since at this point we know that 'instance' is a subtype
|
|
# of (erased) 'template', therefore it defines all protocol members
|
|
if class_obj:
|
|
# For class objects we must only infer constraints if possible, otherwise it
|
|
# can lead to confusion between class and instance, for example StrEnum is
|
|
# Iterable[str] for an instance, but Iterable[StrEnum] for a class object.
|
|
if not mypy.subtypes.is_subtype(
|
|
inst, erase_typevars(temp), ignore_pos_arg_names=True
|
|
):
|
|
continue
|
|
# This exception matches the one in typeops.py, see PR #14121 for context.
|
|
if member == "__call__" and instance.type.is_metaclass(precise=True):
|
|
continue
|
|
res.extend(infer_constraints(temp, inst, self.direction))
|
|
if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol):
|
|
# Settable members are invariant, add opposite constraints
|
|
res.extend(infer_constraints(temp, inst, neg_op(self.direction)))
|
|
return res
|
|
|
|
def visit_callable_type(self, template: CallableType) -> list[Constraint]:
|
|
# Normalize callables before matching against each other.
|
|
# Note that non-normalized callables can be created in annotations
|
|
# using e.g. callback protocols.
|
|
# TODO: check that callables match? Ideally we should not infer constraints
|
|
# callables that can never be subtypes of one another in given direction.
|
|
template = template.with_unpacked_kwargs().with_normalized_var_args()
|
|
extra_tvars = False
|
|
if isinstance(self.actual, CallableType):
|
|
res: list[Constraint] = []
|
|
cactual = self.actual.with_unpacked_kwargs().with_normalized_var_args()
|
|
param_spec = template.param_spec()
|
|
|
|
template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type
|
|
if template.type_guard is not None and cactual.type_guard is not None:
|
|
template_ret_type = template.type_guard
|
|
cactual_ret_type = cactual.type_guard
|
|
|
|
if template.type_is is not None and cactual.type_is is not None:
|
|
template_ret_type = template.type_is
|
|
cactual_ret_type = cactual.type_is
|
|
|
|
res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction))
|
|
|
|
if param_spec is None:
|
|
# TODO: Erase template variables if it is generic?
|
|
if (
|
|
type_state.infer_polymorphic
|
|
and cactual.variables
|
|
and not self.skip_neg_op
|
|
# Technically, the correct inferred type for application of e.g.
|
|
# Callable[..., T] -> Callable[..., T] (with literal ellipsis), to a generic
|
|
# like U -> U, should be Callable[..., Any], but if U is a self-type, we can
|
|
# allow it to leak, to be later bound to self. A bunch of existing code
|
|
# depends on this old behaviour.
|
|
and not (
|
|
any(tv.id.is_self() for tv in cactual.variables)
|
|
and template.is_ellipsis_args
|
|
)
|
|
):
|
|
# If the actual callable is generic, infer constraints in the opposite
|
|
# direction, and indicate to the solver there are extra type variables
|
|
# to solve for (see more details in mypy/solve.py).
|
|
res.extend(
|
|
infer_constraints(
|
|
cactual, template, neg_op(self.direction), skip_neg_op=True
|
|
)
|
|
)
|
|
extra_tvars = True
|
|
|
|
# We can't infer constraints from arguments if the template is Callable[..., T]
|
|
# (with literal '...').
|
|
if not template.is_ellipsis_args:
|
|
unpack_present = find_unpack_in_list(template.arg_types)
|
|
# When both ParamSpec and TypeVarTuple are present, things become messy
|
|
# quickly. For now, we only allow ParamSpec to "capture" TypeVarTuple,
|
|
# but not vice versa.
|
|
# TODO: infer more from prefixes when possible.
|
|
if unpack_present is not None and not cactual.param_spec():
|
|
# We need to re-normalize args to the form they appear in tuples,
|
|
# for callables we always pack the suffix inside another tuple.
|
|
unpack = template.arg_types[unpack_present]
|
|
assert isinstance(unpack, UnpackType)
|
|
tuple_type = get_tuple_fallback_from_unpack(unpack)
|
|
template_types = repack_callable_args(template, tuple_type)
|
|
actual_types = repack_callable_args(cactual, tuple_type)
|
|
# Now we can use the same general helper as for tuple types.
|
|
unpack_constraints = build_constraints_for_simple_unpack(
|
|
template_types, actual_types, neg_op(self.direction)
|
|
)
|
|
res.extend(unpack_constraints)
|
|
else:
|
|
# TODO: do we need some special-casing when unpack is present in actual
|
|
# callable but not in template callable?
|
|
res.extend(
|
|
infer_callable_arguments_constraints(template, cactual, self.direction)
|
|
)
|
|
else:
|
|
prefix = param_spec.prefix
|
|
prefix_len = len(prefix.arg_types)
|
|
cactual_ps = cactual.param_spec()
|
|
|
|
if type_state.infer_polymorphic and cactual.variables and not self.skip_neg_op:
|
|
# Similar logic to the branch above.
|
|
res.extend(
|
|
infer_constraints(
|
|
cactual, template, neg_op(self.direction), skip_neg_op=True
|
|
)
|
|
)
|
|
extra_tvars = True
|
|
|
|
# Compare prefixes as well
|
|
cactual_prefix = cactual.copy_modified(
|
|
arg_types=cactual.arg_types[:prefix_len],
|
|
arg_kinds=cactual.arg_kinds[:prefix_len],
|
|
arg_names=cactual.arg_names[:prefix_len],
|
|
)
|
|
res.extend(
|
|
infer_callable_arguments_constraints(prefix, cactual_prefix, self.direction)
|
|
)
|
|
|
|
param_spec_target: Type | None = None
|
|
if not cactual_ps:
|
|
max_prefix_len = len([k for k in cactual.arg_kinds if k in (ARG_POS, ARG_OPT)])
|
|
prefix_len = min(prefix_len, max_prefix_len)
|
|
param_spec_target = Parameters(
|
|
arg_types=cactual.arg_types[prefix_len:],
|
|
arg_kinds=cactual.arg_kinds[prefix_len:],
|
|
arg_names=cactual.arg_names[prefix_len:],
|
|
variables=cactual.variables if not type_state.infer_polymorphic else [],
|
|
imprecise_arg_kinds=cactual.imprecise_arg_kinds,
|
|
)
|
|
else:
|
|
if len(param_spec.prefix.arg_types) <= len(cactual_ps.prefix.arg_types):
|
|
param_spec_target = cactual_ps.copy_modified(
|
|
prefix=Parameters(
|
|
arg_types=cactual_ps.prefix.arg_types[prefix_len:],
|
|
arg_kinds=cactual_ps.prefix.arg_kinds[prefix_len:],
|
|
arg_names=cactual_ps.prefix.arg_names[prefix_len:],
|
|
imprecise_arg_kinds=cactual_ps.prefix.imprecise_arg_kinds,
|
|
)
|
|
)
|
|
if param_spec_target is not None:
|
|
res.append(Constraint(param_spec, self.direction, param_spec_target))
|
|
if extra_tvars:
|
|
for c in res:
|
|
c.extra_tvars += cactual.variables
|
|
return res
|
|
elif isinstance(self.actual, AnyType):
|
|
param_spec = template.param_spec()
|
|
any_type = AnyType(TypeOfAny.from_another_any, source_any=self.actual)
|
|
if param_spec is None:
|
|
# FIX what if generic
|
|
res = self.infer_against_any(template.arg_types, self.actual)
|
|
else:
|
|
res = [
|
|
Constraint(
|
|
param_spec,
|
|
SUBTYPE_OF,
|
|
Parameters([any_type, any_type], [ARG_STAR, ARG_STAR2], [None, None]),
|
|
)
|
|
]
|
|
res.extend(infer_constraints(template.ret_type, any_type, self.direction))
|
|
return res
|
|
elif isinstance(self.actual, Overloaded):
|
|
return self.infer_against_overloaded(self.actual, template)
|
|
elif isinstance(self.actual, TypeType):
|
|
return infer_constraints(template.ret_type, self.actual.item, self.direction)
|
|
elif isinstance(self.actual, Instance):
|
|
# Instances with __call__ method defined are considered structural
|
|
# subtypes of Callable with a compatible signature.
|
|
call = mypy.subtypes.find_member(
|
|
"__call__", self.actual, self.actual, is_operator=True
|
|
)
|
|
if call:
|
|
return infer_constraints(template, call, self.direction)
|
|
else:
|
|
return []
|
|
else:
|
|
return []
|
|
|
|
def infer_against_overloaded(
|
|
self, overloaded: Overloaded, template: CallableType
|
|
) -> list[Constraint]:
|
|
# Create constraints by matching an overloaded type against a template.
|
|
# This is tricky to do in general. We cheat by only matching against
|
|
# the first overload item that is callable compatible. This
|
|
# seems to work somewhat well, but we should really use a more
|
|
# reliable technique.
|
|
item = find_matching_overload_item(overloaded, template)
|
|
return infer_constraints(template, item, self.direction)
|
|
|
|
def visit_tuple_type(self, template: TupleType) -> list[Constraint]:
|
|
actual = self.actual
|
|
unpack_index = find_unpack_in_list(template.items)
|
|
is_varlength_tuple = (
|
|
isinstance(actual, Instance) and actual.type.fullname == "builtins.tuple"
|
|
)
|
|
|
|
if isinstance(actual, TupleType) or is_varlength_tuple:
|
|
res: list[Constraint] = []
|
|
if unpack_index is not None:
|
|
if is_varlength_tuple:
|
|
# Variadic tuple can be only a supertype of a tuple type, but even if
|
|
# direction is opposite, inferring something may give better error messages.
|
|
unpack_type = template.items[unpack_index]
|
|
assert isinstance(unpack_type, UnpackType)
|
|
unpacked_type = get_proper_type(unpack_type.type)
|
|
if isinstance(unpacked_type, TypeVarTupleType):
|
|
res = [
|
|
Constraint(type_var=unpacked_type, op=self.direction, target=actual)
|
|
]
|
|
else:
|
|
assert (
|
|
isinstance(unpacked_type, Instance)
|
|
and unpacked_type.type.fullname == "builtins.tuple"
|
|
)
|
|
res = infer_constraints(unpacked_type, actual, self.direction)
|
|
assert isinstance(actual, Instance) # ensured by is_varlength_tuple == True
|
|
for i, ti in enumerate(template.items):
|
|
if i == unpack_index:
|
|
# This one we just handled above.
|
|
continue
|
|
# For Tuple[T, *Ts, S] <: tuple[X, ...] infer also T <: X and S <: X.
|
|
res.extend(infer_constraints(ti, actual.args[0], self.direction))
|
|
return res
|
|
else:
|
|
assert isinstance(actual, TupleType)
|
|
unpack_constraints = build_constraints_for_simple_unpack(
|
|
template.items, actual.items, self.direction
|
|
)
|
|
actual_items: tuple[Type, ...] = ()
|
|
template_items: tuple[Type, ...] = ()
|
|
res.extend(unpack_constraints)
|
|
elif isinstance(actual, TupleType):
|
|
a_unpack_index = find_unpack_in_list(actual.items)
|
|
if a_unpack_index is not None:
|
|
# The case where template tuple doesn't have an unpack, but actual tuple
|
|
# has an unpack. We can infer something if actual unpack is a variadic tuple.
|
|
# Tuple[T, S, U] <: tuple[X, *tuple[Y, ...], Z] => T <: X, S <: Y, U <: Z.
|
|
a_unpack = actual.items[a_unpack_index]
|
|
assert isinstance(a_unpack, UnpackType)
|
|
a_unpacked = get_proper_type(a_unpack.type)
|
|
if len(actual.items) + 1 <= len(template.items):
|
|
a_prefix_len = a_unpack_index
|
|
a_suffix_len = len(actual.items) - a_unpack_index - 1
|
|
t_prefix, t_middle, t_suffix = split_with_prefix_and_suffix(
|
|
tuple(template.items), a_prefix_len, a_suffix_len
|
|
)
|
|
actual_items = tuple(actual.items[:a_prefix_len])
|
|
if a_suffix_len:
|
|
actual_items += tuple(actual.items[-a_suffix_len:])
|
|
template_items = t_prefix + t_suffix
|
|
if isinstance(a_unpacked, Instance):
|
|
assert a_unpacked.type.fullname == "builtins.tuple"
|
|
for tm in t_middle:
|
|
res.extend(
|
|
infer_constraints(tm, a_unpacked.args[0], self.direction)
|
|
)
|
|
else:
|
|
actual_items = ()
|
|
template_items = ()
|
|
else:
|
|
actual_items = tuple(actual.items)
|
|
template_items = tuple(template.items)
|
|
else:
|
|
return res
|
|
|
|
# Cases above will return if actual wasn't a TupleType.
|
|
assert isinstance(actual, TupleType)
|
|
if len(actual_items) == len(template_items):
|
|
if (
|
|
actual.partial_fallback.type.is_named_tuple
|
|
and template.partial_fallback.type.is_named_tuple
|
|
):
|
|
# For named tuples using just the fallbacks usually gives better results.
|
|
return res + infer_constraints(
|
|
template.partial_fallback, actual.partial_fallback, self.direction
|
|
)
|
|
for i in range(len(template_items)):
|
|
res.extend(
|
|
infer_constraints(template_items[i], actual_items[i], self.direction)
|
|
)
|
|
res.extend(
|
|
infer_constraints(
|
|
template.partial_fallback, actual.partial_fallback, self.direction
|
|
)
|
|
)
|
|
return res
|
|
elif isinstance(actual, AnyType):
|
|
return self.infer_against_any(template.items, actual)
|
|
else:
|
|
return []
|
|
|
|
def visit_typeddict_type(self, template: TypedDictType) -> list[Constraint]:
|
|
actual = self.actual
|
|
if isinstance(actual, TypedDictType):
|
|
res: list[Constraint] = []
|
|
# NOTE: Non-matching keys are ignored. Compatibility is checked
|
|
# elsewhere so this shouldn't be unsafe.
|
|
for item_name, template_item_type, actual_item_type in template.zip(actual):
|
|
res.extend(infer_constraints(template_item_type, actual_item_type, self.direction))
|
|
return res
|
|
elif isinstance(actual, AnyType):
|
|
return self.infer_against_any(template.items.values(), actual)
|
|
else:
|
|
return []
|
|
|
|
def visit_union_type(self, template: UnionType) -> list[Constraint]:
|
|
assert False, (
|
|
"Unexpected UnionType in ConstraintBuilderVisitor"
|
|
" (should have been handled in infer_constraints)"
|
|
)
|
|
|
|
def visit_type_alias_type(self, template: TypeAliasType) -> list[Constraint]:
|
|
assert False, f"This should be never called, got {template}"
|
|
|
|
def infer_against_any(self, types: Iterable[Type], any_type: AnyType) -> list[Constraint]:
|
|
res: list[Constraint] = []
|
|
# Some items may be things like `*Tuple[*Ts, T]` for example from callable types with
|
|
# suffix after *arg, so flatten them.
|
|
for t in flatten_nested_tuples(types):
|
|
if isinstance(t, UnpackType):
|
|
if isinstance(t.type, TypeVarTupleType):
|
|
res.append(Constraint(t.type, self.direction, any_type))
|
|
else:
|
|
unpacked = get_proper_type(t.type)
|
|
assert isinstance(unpacked, Instance)
|
|
res.extend(infer_constraints(unpacked, any_type, self.direction))
|
|
else:
|
|
# Note that we ignore variance and simply always use the
|
|
# original direction. This is because for Any targets direction is
|
|
# irrelevant in most cases, see e.g. is_same_constraint().
|
|
res.extend(infer_constraints(t, any_type, self.direction))
|
|
return res
|
|
|
|
def visit_overloaded(self, template: Overloaded) -> list[Constraint]:
|
|
if isinstance(self.actual, CallableType):
|
|
items = find_matching_overload_items(template, self.actual)
|
|
else:
|
|
items = template.items
|
|
res: list[Constraint] = []
|
|
for t in items:
|
|
res.extend(infer_constraints(t, self.actual, self.direction))
|
|
return res
|
|
|
|
def visit_type_type(self, template: TypeType) -> list[Constraint]:
|
|
if isinstance(self.actual, CallableType):
|
|
return infer_constraints(template.item, self.actual.ret_type, self.direction)
|
|
elif isinstance(self.actual, Overloaded):
|
|
return infer_constraints(template.item, self.actual.items[0].ret_type, self.direction)
|
|
elif isinstance(self.actual, TypeType):
|
|
return infer_constraints(template.item, self.actual.item, self.direction)
|
|
elif isinstance(self.actual, AnyType):
|
|
return infer_constraints(template.item, self.actual, self.direction)
|
|
else:
|
|
return []
|
|
|
|
|
|
def neg_op(op: int) -> int:
|
|
"""Map SubtypeOf to SupertypeOf and vice versa."""
|
|
|
|
if op == SUBTYPE_OF:
|
|
return SUPERTYPE_OF
|
|
elif op == SUPERTYPE_OF:
|
|
return SUBTYPE_OF
|
|
else:
|
|
raise ValueError(f"Invalid operator {op}")
|
|
|
|
|
|
def find_matching_overload_item(overloaded: Overloaded, template: CallableType) -> CallableType:
|
|
"""Disambiguate overload item against a template."""
|
|
items = overloaded.items
|
|
for item in items:
|
|
# Return type may be indeterminate in the template, so ignore it when performing a
|
|
# subtype check.
|
|
if mypy.subtypes.is_callable_compatible(
|
|
item,
|
|
template,
|
|
is_compat=mypy.subtypes.is_subtype,
|
|
is_proper_subtype=False,
|
|
ignore_return=True,
|
|
):
|
|
return item
|
|
# Fall back to the first item if we can't find a match. This is totally arbitrary --
|
|
# maybe we should just bail out at this point.
|
|
return items[0]
|
|
|
|
|
|
def find_matching_overload_items(
|
|
overloaded: Overloaded, template: CallableType
|
|
) -> list[CallableType]:
|
|
"""Like find_matching_overload_item, but return all matches, not just the first."""
|
|
items = overloaded.items
|
|
res = []
|
|
for item in items:
|
|
# Return type may be indeterminate in the template, so ignore it when performing a
|
|
# subtype check.
|
|
if mypy.subtypes.is_callable_compatible(
|
|
item,
|
|
template,
|
|
is_compat=mypy.subtypes.is_subtype,
|
|
is_proper_subtype=False,
|
|
ignore_return=True,
|
|
):
|
|
res.append(item)
|
|
if not res:
|
|
# Falling back to all items if we can't find a match is pretty arbitrary, but
|
|
# it maintains backward compatibility.
|
|
res = items.copy()
|
|
return res
|
|
|
|
|
|
def get_tuple_fallback_from_unpack(unpack: UnpackType) -> TypeInfo:
|
|
"""Get builtins.tuple type from available types to construct homogeneous tuples."""
|
|
tp = get_proper_type(unpack.type)
|
|
if isinstance(tp, Instance) and tp.type.fullname == "builtins.tuple":
|
|
return tp.type
|
|
if isinstance(tp, TypeVarTupleType):
|
|
return tp.tuple_fallback.type
|
|
if isinstance(tp, TupleType):
|
|
for base in tp.partial_fallback.type.mro:
|
|
if base.fullname == "builtins.tuple":
|
|
return base
|
|
assert False, "Invalid unpack type"
|
|
|
|
|
|
def repack_callable_args(callable: CallableType, tuple_type: TypeInfo) -> list[Type]:
|
|
"""Present callable with star unpack in a normalized form.
|
|
|
|
Since positional arguments cannot follow star argument, they are packed in a suffix,
|
|
while prefix is represented as individual positional args. We want to put all in a single
|
|
list with unpack in the middle, and prefix/suffix on the sides (as they would appear
|
|
in e.g. a TupleType).
|
|
"""
|
|
if ARG_STAR not in callable.arg_kinds:
|
|
return callable.arg_types
|
|
star_index = callable.arg_kinds.index(ARG_STAR)
|
|
arg_types = callable.arg_types[:star_index]
|
|
star_type = callable.arg_types[star_index]
|
|
suffix_types = []
|
|
if not isinstance(star_type, UnpackType):
|
|
# Re-normalize *args: X -> *args: *tuple[X, ...]
|
|
star_type = UnpackType(Instance(tuple_type, [star_type]))
|
|
else:
|
|
tp = get_proper_type(star_type.type)
|
|
if isinstance(tp, TupleType):
|
|
assert isinstance(tp.items[0], UnpackType)
|
|
star_type = tp.items[0]
|
|
suffix_types = tp.items[1:]
|
|
return arg_types + [star_type] + suffix_types
|
|
|
|
|
|
def build_constraints_for_simple_unpack(
|
|
template_args: list[Type], actual_args: list[Type], direction: int
|
|
) -> list[Constraint]:
|
|
"""Infer constraints between two lists of types with variadic items.
|
|
|
|
This function is only supposed to be called when a variadic item is present in templates.
|
|
If there is no variadic item the actuals, we simply use split_with_prefix_and_suffix()
|
|
and infer prefix <: prefix, suffix <: suffix, variadic <: middle. If there is a variadic
|
|
item in the actuals we need to be more careful, only common prefix/suffix can generate
|
|
constraints, also we can only infer constraints for variadic template item, if template
|
|
prefix/suffix are shorter that actual ones, otherwise there may be partial overlap
|
|
between variadic items, for example if template prefix is longer:
|
|
|
|
templates: T1, T2, Ts, Ts, Ts, ...
|
|
actuals: A1, As, As, As, ...
|
|
|
|
Note: this function can only be called for builtin variadic constructors: Tuple and Callable.
|
|
For instances, you should first find correct type argument mapping.
|
|
"""
|
|
template_unpack = find_unpack_in_list(template_args)
|
|
assert template_unpack is not None
|
|
template_prefix = template_unpack
|
|
template_suffix = len(template_args) - template_prefix - 1
|
|
|
|
t_unpack = None
|
|
res = []
|
|
|
|
actual_unpack = find_unpack_in_list(actual_args)
|
|
if actual_unpack is None:
|
|
t_unpack = template_args[template_unpack]
|
|
if template_prefix + template_suffix > len(actual_args):
|
|
# These can't be subtypes of each-other, return fast.
|
|
assert isinstance(t_unpack, UnpackType)
|
|
if isinstance(t_unpack.type, TypeVarTupleType):
|
|
# Set TypeVarTuple to empty to improve error messages.
|
|
return [
|
|
Constraint(
|
|
t_unpack.type, direction, TupleType([], t_unpack.type.tuple_fallback)
|
|
)
|
|
]
|
|
else:
|
|
return []
|
|
common_prefix = template_prefix
|
|
common_suffix = template_suffix
|
|
else:
|
|
actual_prefix = actual_unpack
|
|
actual_suffix = len(actual_args) - actual_prefix - 1
|
|
common_prefix = min(template_prefix, actual_prefix)
|
|
common_suffix = min(template_suffix, actual_suffix)
|
|
if actual_prefix >= template_prefix and actual_suffix >= template_suffix:
|
|
# This is the only case where we can guarantee there will be no partial overlap
|
|
# (note however partial overlap is OK for variadic tuples, it is handled below).
|
|
t_unpack = template_args[template_unpack]
|
|
|
|
# Handle constraints from prefixes/suffixes first.
|
|
start, middle, end = split_with_prefix_and_suffix(
|
|
tuple(actual_args), common_prefix, common_suffix
|
|
)
|
|
for t, a in zip(template_args[:common_prefix], start):
|
|
res.extend(infer_constraints(t, a, direction))
|
|
if common_suffix:
|
|
for t, a in zip(template_args[-common_suffix:], end):
|
|
res.extend(infer_constraints(t, a, direction))
|
|
|
|
if t_unpack is not None:
|
|
# Add constraint(s) for variadic item when possible.
|
|
assert isinstance(t_unpack, UnpackType)
|
|
tp = get_proper_type(t_unpack.type)
|
|
if isinstance(tp, Instance) and tp.type.fullname == "builtins.tuple":
|
|
# Homogeneous case *tuple[T, ...] <: [X, Y, Z, ...].
|
|
for a in middle:
|
|
# TODO: should we use union instead of join here?
|
|
if not isinstance(a, UnpackType):
|
|
res.extend(infer_constraints(tp.args[0], a, direction))
|
|
else:
|
|
a_tp = get_proper_type(a.type)
|
|
# This is the case *tuple[T, ...] <: *tuple[A, ...].
|
|
if isinstance(a_tp, Instance) and a_tp.type.fullname == "builtins.tuple":
|
|
res.extend(infer_constraints(tp.args[0], a_tp.args[0], direction))
|
|
elif isinstance(tp, TypeVarTupleType):
|
|
res.append(Constraint(tp, direction, TupleType(list(middle), tp.tuple_fallback)))
|
|
elif actual_unpack is not None:
|
|
# A special case for a variadic tuple unpack, we simply infer T <: X from
|
|
# Tuple[..., *tuple[T, ...], ...] <: Tuple[..., *tuple[X, ...], ...].
|
|
actual_unpack_type = actual_args[actual_unpack]
|
|
assert isinstance(actual_unpack_type, UnpackType)
|
|
a_unpacked = get_proper_type(actual_unpack_type.type)
|
|
if isinstance(a_unpacked, Instance) and a_unpacked.type.fullname == "builtins.tuple":
|
|
t_unpack = template_args[template_unpack]
|
|
assert isinstance(t_unpack, UnpackType)
|
|
tp = get_proper_type(t_unpack.type)
|
|
if isinstance(tp, Instance) and tp.type.fullname == "builtins.tuple":
|
|
res.extend(infer_constraints(tp.args[0], a_unpacked.args[0], direction))
|
|
return res
|
|
|
|
|
|
def infer_directed_arg_constraints(left: Type, right: Type, direction: int) -> list[Constraint]:
|
|
"""Infer constraints between two arguments using direction between original callables."""
|
|
if isinstance(left, (ParamSpecType, UnpackType)) or isinstance(
|
|
right, (ParamSpecType, UnpackType)
|
|
):
|
|
# This avoids bogus constraints like T <: P.args
|
|
# TODO: can we infer something useful for *T vs P?
|
|
return []
|
|
if direction == SUBTYPE_OF:
|
|
# We invert direction to account for argument contravariance.
|
|
return infer_constraints(left, right, neg_op(direction))
|
|
else:
|
|
return infer_constraints(right, left, neg_op(direction))
|
|
|
|
|
|
def infer_callable_arguments_constraints(
|
|
template: NormalizedCallableType | Parameters,
|
|
actual: NormalizedCallableType | Parameters,
|
|
direction: int,
|
|
) -> list[Constraint]:
|
|
"""Infer constraints between argument types of two callables.
|
|
|
|
This function essentially extracts four steps from are_parameters_compatible() in
|
|
subtypes.py that involve subtype checks between argument types. We keep the argument
|
|
matching logic, but ignore various strictness flags present there, and checks that
|
|
do not involve subtyping. Then in place of every subtype check we put an infer_constraints()
|
|
call for the same types.
|
|
"""
|
|
res = []
|
|
if direction == SUBTYPE_OF:
|
|
left, right = template, actual
|
|
else:
|
|
left, right = actual, template
|
|
left_star = left.var_arg()
|
|
left_star2 = left.kw_arg()
|
|
right_star = right.var_arg()
|
|
right_star2 = right.kw_arg()
|
|
|
|
# Numbering of steps below matches the one in are_parameters_compatible() for convenience.
|
|
# Phase 1a: compare star vs star arguments.
|
|
if left_star is not None and right_star is not None:
|
|
res.extend(infer_directed_arg_constraints(left_star.typ, right_star.typ, direction))
|
|
if left_star2 is not None and right_star2 is not None:
|
|
res.extend(infer_directed_arg_constraints(left_star2.typ, right_star2.typ, direction))
|
|
|
|
# Phase 1b: compare left args with corresponding non-star right arguments.
|
|
for right_arg in right.formal_arguments():
|
|
left_arg = mypy.typeops.callable_corresponding_argument(left, right_arg)
|
|
if left_arg is None:
|
|
continue
|
|
res.extend(infer_directed_arg_constraints(left_arg.typ, right_arg.typ, direction))
|
|
|
|
# Phase 1c: compare left args with right *args.
|
|
if right_star is not None:
|
|
right_by_position = right.try_synthesizing_arg_from_vararg(None)
|
|
assert right_by_position is not None
|
|
i = right_star.pos
|
|
assert i is not None
|
|
while i < len(left.arg_kinds) and left.arg_kinds[i].is_positional():
|
|
left_by_position = left.argument_by_position(i)
|
|
assert left_by_position is not None
|
|
res.extend(
|
|
infer_directed_arg_constraints(
|
|
left_by_position.typ, right_by_position.typ, direction
|
|
)
|
|
)
|
|
i += 1
|
|
|
|
# Phase 1d: compare left args with right **kwargs.
|
|
if right_star2 is not None:
|
|
right_names = {name for name in right.arg_names if name is not None}
|
|
left_only_names = set()
|
|
for name, kind in zip(left.arg_names, left.arg_kinds):
|
|
if name is None or kind.is_star() or name in right_names:
|
|
continue
|
|
left_only_names.add(name)
|
|
|
|
right_by_name = right.try_synthesizing_arg_from_kwarg(None)
|
|
assert right_by_name is not None
|
|
for name in left_only_names:
|
|
left_by_name = left.argument_by_name(name)
|
|
assert left_by_name is not None
|
|
res.extend(
|
|
infer_directed_arg_constraints(left_by_name.typ, right_by_name.typ, direction)
|
|
)
|
|
return res
|
|
|
|
|
|
def filter_imprecise_kinds(cs: list[Constraint]) -> list[Constraint]:
|
|
"""For each ParamSpec remove all imprecise constraints, if at least one precise available."""
|
|
have_precise = set()
|
|
for c in cs:
|
|
if not isinstance(c.origin_type_var, ParamSpecType):
|
|
continue
|
|
if (
|
|
isinstance(c.target, ParamSpecType)
|
|
or isinstance(c.target, Parameters)
|
|
and not c.target.imprecise_arg_kinds
|
|
):
|
|
have_precise.add(c.type_var)
|
|
new_cs = []
|
|
for c in cs:
|
|
if not isinstance(c.origin_type_var, ParamSpecType) or c.type_var not in have_precise:
|
|
new_cs.append(c)
|
|
if not isinstance(c.target, Parameters) or not c.target.imprecise_arg_kinds:
|
|
new_cs.append(c)
|
|
return new_cs
|