diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml index 654cdf2c5cf11..c4a64c4a9696d 100644 --- a/.github/workflows/mypy_primer.yml +++ b/.github/workflows/mypy_primer.yml @@ -67,6 +67,7 @@ jobs: --debug \ --additional-flags="--debug-serialize" \ --output concise \ + --show-speed-regression \ --mypy-install-librt \ | tee diff_${{ matrix.shard-index }}.txt ) || [ $? -eq 1 ] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8b40fbb592789..af9f7671270c2 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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 ( @@ -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, @@ -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 @@ -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 @@ -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 @@ -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. @@ -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, @@ -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. """ @@ -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, @@ -6287,7 +6340,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 ): diff --git a/mypy/constraints.py b/mypy/constraints.py index e54983c9744f9..aa095316c234e 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -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): @@ -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) diff --git a/mypy/infer.py b/mypy/infer.py index 2c155ee2456b3..d514900e66d3b 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -8,6 +8,7 @@ from mypy.constraints import ( SUBTYPE_OF, SUPERTYPE_OF, + Constraint, infer_constraints, infer_constraints_for_callable, ) @@ -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. @@ -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 @@ -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 diff --git a/mypy/join.py b/mypy/join.py index c609b303bb087..b52eb8a6f69e0 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -809,8 +809,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) @@ -867,8 +870,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) diff --git a/mypy/messages.py b/mypy/messages.py index 0f3a5be368a4b..8b4c1354e4f4a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -3129,7 +3129,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: diff --git a/mypy/typeops.py b/mypy/typeops.py index eb9d0d1474b8b..d3e2ad9a7e311 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -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 ( @@ -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), diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 445b5ce26ee4a..d665476b907e4 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3957,3 +3957,26 @@ 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]) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 3735f0183eb4a..01eed498736ea 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1629,3 +1629,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(xs: 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)" diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 2441770b5598e..0e3d5ab451172 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -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] diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index d52cb575bc916..7af7a55e73f39 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -480,3 +480,26 @@ reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]" [builtins fixtures/tuple.pyi] + +[case testInferenceContextBothSidesSlack] +from typing import Never + +class Vec[T]: # invariant in T + def _items(self) -> list[T]: ... + def _append(self, item: T) -> None: ... + +def concat_vecs[T1, T2, T_slack=Never]( + left: Vec[T1], + right: Vec[T2], +) -> Vec[T1 | T2 | T_slack]: + return Vec() + +class A: ... +class B: ... +class C: ... + +def test_vec_concat(left: Vec[A], right: Vec[B]) -> None: + reveal_type(concat_vecs(left, right)) # N: Revealed type is "__main__.Vec[__main__.A | __main__.B]" + _0: Vec[A | B] = concat_vecs(left, right) # OK + _1: Vec[A | B | C] = concat_vecs(left, right) # OK + _2: Vec[object] = concat_vecs(left, right) # OK diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 5d81e76ba8c6e..81d1b5dacca59 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -15,6 +15,15 @@ class Bad: ... x = ["foo", {"bar": [Bad()]}] # E: List item 0 has incompatible type "Bad"; expected "str | list[JSON] | dict[str, JSON]" [builtins fixtures/isinstancelist.pyi] +[case testRecursiveAliasNestedWithAny] +from typing import Any, Dict, List, Union + +JSON = Union[str, List[JSON], Dict[str, JSON]] + +a: Any +x: JSON = {"str": [{"bar": a}]} +[builtins fixtures/isinstancelist.pyi] + [case testRecursiveAliasBasicGenericSubtype] from typing import Union, TypeVar, Sequence, List @@ -298,8 +307,8 @@ a = [[b]] # OK b = aa # E: Incompatible types in assignment (expression has type "Nested[A]", variable has type "B") def join(a: T, b: T) -> T: ... -reveal_type(join(a, b)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" -reveal_type(join(b, a)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" +reveal_type(join(aa, b)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" +reveal_type(join(b, aa)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" [builtins fixtures/isinstancelist.pyi] [case testRecursiveAliasWithRecursiveInstanceInference]