Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
82 changes: 70 additions & 12 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mypy.checkmember import analyze_member_access, has_operator
from mypy.checkstrformat import StringFormatterChecker
from mypy.constant_fold import constant_fold_expr
from mypy.constraints import Constraint
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error
from mypy.expandtype import (
Expand Down Expand Up @@ -1792,8 +1793,10 @@ def check_callable_call(
callee = freshen_function_type_vars(callee)
self.freshen_cache[(context, original_callee)] = callee

initial_callee = callee
constraints: list[Constraint] = []
if callee.is_generic():
callee = self.infer_function_type_arguments_using_context(callee, context)
callee, constraints = self.infer_function_type_arguments_using_context(callee, context)

formal_to_actual = map_actuals_to_formals(
arg_kinds,
Expand Down Expand Up @@ -1825,12 +1828,20 @@ def check_callable_call(
)
callee = callee.copy_modified(arg_types=[new_arg_type])

if callee.is_generic():
if initial_callee.is_generic():
need_refresh = any(
isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables
isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in initial_callee.variables
)
callee = self.infer_function_type_arguments(
callee, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context
callee,
initial_callee,
constraints,
args,
arg_kinds,
arg_names,
formal_to_actual,
need_refresh,
context,
)
if need_refresh:
# Argument kinds etc. may have changed due to
Expand Down Expand Up @@ -2113,7 +2124,7 @@ def infer_arg_types_in_context(

def infer_function_type_arguments_using_context(
self, callable: CallableType, error_context: Context
) -> CallableType:
) -> tuple[CallableType, list[Constraint]]:
"""Unify callable return type to type context to infer type vars.

For example, if the return type is set[t] where 't' is a type variable
Expand All @@ -2122,7 +2133,7 @@ def infer_function_type_arguments_using_context(
"""
ctx = self.type_context[-1]
if not ctx:
return callable
return callable, []
# The return type may have references to type metavariables that
# we are inferring right now. We must consider them as indeterminate
# and they are not potential results; thus we replace them with the
Expand Down Expand Up @@ -2179,8 +2190,8 @@ def infer_function_type_arguments_using_context(
# TODO: we may want to add similar exception if all arguments are lambdas, since
# in this case external context is almost everything we have.
if not is_generic_instance(ctx) and not is_literal_type_like(ctx):
return callable.copy_modified()
args = infer_type_arguments(
return callable.copy_modified(), []
args, constraints = infer_type_arguments(
callable.variables, ret_type, erased_ctx, skip_unsatisfied=True
)
# Only substitute non-Uninhabited and non-erased types.
Expand All @@ -2192,13 +2203,16 @@ def infer_function_type_arguments_using_context(
new_args.append(arg)
# Don't show errors after we have only used the outer context for inference.
# We will use argument context to infer more variables.
return self.apply_generic_arguments(
callable, new_args, error_context, skip_unsatisfied=True
return (
self.apply_generic_arguments(callable, new_args, error_context, skip_unsatisfied=True),
constraints,
)

def infer_function_type_arguments(
self,
callee_type: CallableType,
initial_callee_type: CallableType,
ret_constraints: list[Constraint],
args: list[Expression],
arg_kinds: list[ArgKind],
arg_names: Sequence[str | None] | None,
Expand All @@ -2208,7 +2222,8 @@ def infer_function_type_arguments(
) -> CallableType:
"""Infer the type arguments for a generic callee type.

Infer based on the types of arguments.
Infer based on the types of arguments. The `initial_callee_type` is callee
type before applying arguments inferred from return type context.

Return a derived callable type that has the arguments applied.
"""
Expand All @@ -2233,6 +2248,44 @@ def infer_function_type_arguments(
else:
pass1_args.append(arg)

# First, try inferring callee type using both argument type and return
# constraints. Do not do this if special_sig is set or for type objects,
# those are special-cased elsewhere.
if (
initial_callee_type != callee_type
and initial_callee_type.special_sig is None
and not initial_callee_type.is_type_obj()
# This is not very principled, but also prefer outer context for
# recursive types (because values can never be recursive).
and not type_state.infer_unions
):
inferred_args, _ = infer_function_type_arguments(
initial_callee_type,
pass1_args,
arg_kinds,
arg_names,
formal_to_actual,
context=self.argument_infer_context(),
strict=self.chk.in_checked_function(),
extra_constraints=ret_constraints,
)
# Only use two-side inference if it is fully successful,
# otherwise, fallback to return type context as a tiebreaker.
if not any(
a is None
or isinstance(get_proper_type(a), UninhabitedType)
or has_erased_component(a)
or set(get_type_vars(a)) & set(callee_type.variables)
for a in inferred_args
):
return self.apply_inferred_arguments(
initial_callee_type, inferred_args, context
)

if not callee_type.is_generic():
# Fast path: return context inferred all type arguments.
return callee_type

inferred_args, _ = infer_function_type_arguments(
callee_type,
pass1_args,
Expand Down Expand Up @@ -6281,7 +6334,12 @@ def accept(
# context, and use enclosing one, see infer_lambda_type_using_context().
# TODO: consider using cache for more expression kinds.
elif (
isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr, MemberExpr))
(
isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr))
# Only cache non-static access, it may be slow, and may freshen type vars.
or isinstance(node, MemberExpr)
and node.node is None
)
and not (self.in_lambda_expr or self.chk.current_node_deferred)
and not self.chk.options.disable_expression_cache
):
Expand Down
10 changes: 9 additions & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,13 @@ def infer_constraints(
for (t, a) in reversed(type_state.inferring)
):
return []
if has_recursive_types(template) or isinstance(get_proper_type(template), Instance):
# This guard matches the one we use for subtyping, it is slightly more risky,
# but it is much faster than has_recursive_types().
if (
isinstance(template, TypeAliasType)
and template.is_recursive
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):
Expand Down Expand Up @@ -529,6 +535,8 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) ->
# 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)]
if not type_var_items or not non_type_var_items:
return []
return infer_constraints(
UnionType.make_union(non_type_var_items), actual, direction
) or infer_constraints(UnionType.make_union(type_var_items), actual, direction)
Expand Down
9 changes: 7 additions & 2 deletions mypy/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from mypy.constraints import (
SUBTYPE_OF,
SUPERTYPE_OF,
Constraint,
infer_constraints,
infer_constraints_for_callable,
)
Expand Down Expand Up @@ -40,6 +41,7 @@ def infer_function_type_arguments(
context: ArgumentInferContext,
strict: bool = True,
allow_polymorphic: bool = False,
extra_constraints: list[Constraint] | None = None,
) -> tuple[list[Type | None], list[TypeVarLikeType]]:
"""Infer the type arguments of a generic function.

Expand All @@ -58,6 +60,8 @@ def infer_function_type_arguments(
constraints = infer_constraints_for_callable(
callee_type, arg_types, arg_kinds, arg_names, formal_to_actual, context
)
if extra_constraints:
constraints += extra_constraints

# Solve constraints.
type_vars = callee_type.variables
Expand All @@ -71,10 +75,11 @@ def infer_type_arguments(
is_supertype: bool = False,
skip_unsatisfied: bool = False,
erase_types: bool = True,
) -> list[Type | None]:
) -> tuple[list[Type | None], list[Constraint]]:
# Like infer_function_type_arguments, but only match a single type
# against a generic type.
constraints = infer_constraints(
template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF, erase_types=erase_types
)
return solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0]
solutions, _ = solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)
return solutions, constraints
10 changes: 8 additions & 2 deletions mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,8 +829,11 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
# The result should always use 'function' as a fallback if either operands are using it.
if t.fallback.type.fullname == "builtins.function":
fallback = t.fallback
else:
elif s.fallback.type.fullname == "builtins.function":
fallback = s.fallback
else:
fallback = InstanceJoiner().join_instances(t.fallback, s.fallback)
assert isinstance(fallback, Instance)
instance_type = None
if t.instance_type is not None and s.instance_type is not None:
instance_type = join_types(t.instance_type, s.instance_type)
Expand Down Expand Up @@ -888,8 +891,11 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
# The result should always use 'function' as a fallback if either operands are using it.
if t.fallback.type.fullname == "builtins.function":
fallback = t.fallback
else:
elif s.fallback.type.fullname == "builtins.function":
fallback = s.fallback
else:
fallback = InstanceJoiner().join_instances(t.fallback, s.fallback)
assert isinstance(fallback, Instance)
instance_type = None
if t.instance_type is not None and s.instance_type is not None:
instance_type = join_types(t.instance_type, s.instance_type)
Expand Down
2 changes: 1 addition & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3136,7 +3136,7 @@ def [T <: int] f(self, x: int, y: T) -> None
# TODO: avoid different logic for incremental runs.
first_arg = get_first_arg(tp)

if tp.is_type_obj():
if tp.is_type_obj() or not tp.is_bound:
skip_self = True
if first_arg and not skip_self:
if s:
Expand Down
4 changes: 2 additions & 2 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def bind_self_inner(
self_vars = [tv for tv in func.variables if tv.id in self_ids]

# Solve for these type arguments using the actual class or instance type.
typeargs = infer_type_arguments(
typeargs, _ = infer_type_arguments(
self_vars, self_param_type, original_type, is_supertype=True, erase_types=False
)
if (
Expand All @@ -561,7 +561,7 @@ def bind_self_inner(
and isinstance(original_type, (Instance, TypeVarType, TupleType))
):
# In case we call a classmethod through an instance x, fallback to type(x).
typeargs = infer_type_arguments(
typeargs, _ = infer_type_arguments(
self_vars,
self_param_type,
TypeType(original_type),
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3957,3 +3957,25 @@ Ex = TypeVar("Ex", covariant=True)

def takes_lambda(func: Callable[[Ex], Any]) -> None:
pass

[case testTypeObjectJoinWithOverloadsCorrectFallback]
from typing import Iterable, overload

class M(type): ...
class A(metaclass=M): ...
class B: ...
class C(metaclass=M):
@overload
def __init__(self, x: int) -> None: ...
@overload
def __init__(self, x: str) -> None: ...
def __init__(self, x):
pass

def f(x: Iterable[object]) -> None: ...
f([A, B, C])
f([B, A, C])
f([B, C, A])
f([A, C, B])
f([C, A, B])
f([C, B, A])
45 changes: 45 additions & 0 deletions test-data/unit/check-inference-context.test
Original file line number Diff line number Diff line change
Expand Up @@ -1635,3 +1635,48 @@ def foo(args: Union[Iterable[Union[str, int]], str, int]) -> Iterable[str]:
)
return args
[builtins fixtures/isinstancelist.pyi]

[case testInferenceContextBothSidesInvariant]
from typing import TypeVar, Generic

T = TypeVar("T")

class B(Generic[T]): ...
class C(B[int]): ...

def first(xs: list[T]) -> T: ...

b: B[int]
cs: list[C]
b = first(cs) # OK
reveal_type(b) # N: Revealed type is "__main__.C"

[case testInferenceContextBothSidesNarrow]
from typing import TypeVar, Generic

T = TypeVar("T")

class B(Generic[T]): ...
class C(B[int]): ...

def same(x: T) -> T: ...

b: B[int]
b = same(C()) # This should narrow
reveal_type(b) # N: Revealed type is "__main__.C"

[case testInferenceContextBothSidesJoin]
from typing import TypeVar, Generic, Callable, Union

T = TypeVar("T")

class A: ...
class B: ...
class C: ...

def make(x: T) -> Callable[[T], None]: ...

x: Union[B, C]
fn: Callable[[Union[A, B]], None]
fn = make(x)
reveal_type(fn) # N: Revealed type is "def (__main__.B | __main__.C | __main__.A)"
18 changes: 16 additions & 2 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -2814,6 +2814,20 @@ x: Union[Any, C[[Any, Any]]]

def test(x: int) -> None: ...

x = foo(test) # E: Argument 1 to "foo" has incompatible type "Callable[[int], None]"; expected "Callable[[Any, Any], None]"
reveal_type(x) # N: Revealed type is "__main__.C[[Any, Any]]"
x = foo(test)
reveal_type(x) # N: Revealed type is "__main__.C[Any] | __main__.C[[Any, Any]]"
[builtins fixtures/paramspec.pyi]

[case testTwoSidedInferenceWithParamSpec]
from typing import Callable, Generic, TypeVar
from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")
class C(Generic[P, T]): ...

def thing(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> C[P, T]: ...

def test(x: int = 0) -> None: ...
f: C[[], None] = thing(test)
[builtins fixtures/paramspec.pyi]
Loading
Loading