diff --git a/mypy/checker.py b/mypy/checker.py index f201a767a860..35a67d188311 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -726,6 +726,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(item, Decorator) item_type = self.extract_callable_type(item.var.type, item) if item_type is not None: + item_type.definition = item item_types.append(item_type) if item_types: defn.type = Overloaded(item_types) @@ -4927,17 +4928,7 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: inplace, method = infer_operator_assignment_method(lvalue_type, s.op) if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) - rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) - if isinstance(inst := get_proper_type(lvalue_type), Instance) and isinstance( - defn := inst.type.get_method(method), OverloadedFuncDef - ): - for item in defn.items: - if ( - isinstance(item, Decorator) - and isinstance(typ := item.func.type, CallableType) - and (bind_self(typ) == method_type) - ): - self.warn_deprecated(item.func, s) + rvalue_type, _ = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -7962,7 +7953,7 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None: node = node.func if ( isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) - and ((deprecated := node.deprecated) is not None) + and (deprecated := node.deprecated) is not None and not self.is_typeshed_stub and not any( node.fullname == p or node.fullname.startswith(f"{p}.") @@ -7972,21 +7963,6 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None: warn = self.msg.note if self.options.report_deprecated_as_note else self.msg.fail warn(deprecated, context, code=codes.DEPRECATED) - def warn_deprecated_overload_item( - self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None - ) -> None: - """Warn if the overload item corresponding to the given callable is deprecated.""" - target = get_proper_type(target) - if isinstance(node, OverloadedFuncDef) and isinstance(target, CallableType): - for item in node.items: - if isinstance(item, Decorator) and isinstance( - candidate := item.func.type, CallableType - ): - if selftype is not None and not node.is_static: - candidate = bind_self(candidate, selftype) - if candidate == target: - self.warn_deprecated(item.func, context) - # leafs def visit_pass_stmt(self, o: PassStmt, /) -> None: diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 7a5e9cb52c70..2a8fbdb0c9f1 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -253,12 +253,6 @@ def check_deprecated(self, node: Node | None, context: Context) -> None: def warn_deprecated(self, node: Node | None, context: Context) -> None: raise NotImplementedError - @abstractmethod - def warn_deprecated_overload_item( - self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None - ) -> None: - raise NotImplementedError - @abstractmethod def type_is_iterable(self, type: Type) -> bool: raise NotImplementedError diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ecae451299d7..bec34eef4983 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -129,7 +129,6 @@ validate_instance, ) from mypy.typeops import ( - bind_self, callable_type, custom_special_method, erase_to_union_or_bound, @@ -1517,15 +1516,6 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) - if isinstance(e.callee, (NameExpr, MemberExpr)): - node = e.callee.node - if node is None and member is not None and isinstance(object_type, Instance): - if (symbol := object_type.type.get(member)) is not None: - node = symbol.node - self.chk.check_deprecated(node, e) - self.chk.warn_deprecated_overload_item( - node, e, target=callee_type, selftype=object_type - ) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: @@ -2943,6 +2933,8 @@ def infer_overload_return_type( # check for ambiguity due to 'Any' below. if not args_contain_any: self.chk.store_types(m) + if isinstance(infer_type, ProperType) and isinstance(infer_type, CallableType): + self.chk.check_deprecated(infer_type.definition, context) return ret_type, infer_type p_infer_type = get_proper_type(infer_type) if isinstance(p_infer_type, CallableType): @@ -2979,6 +2971,11 @@ def infer_overload_return_type( else: # Success! No ambiguity; return the first match. self.chk.store_types(type_maps[0]) + inferred_callable = inferred_types[0] + if isinstance(inferred_callable, ProperType) and isinstance( + inferred_callable, CallableType + ): + self.chk.check_deprecated(inferred_callable.definition, context) return return_types[0], inferred_types[0] def overload_erased_call_targets( @@ -4103,16 +4100,6 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: errors.append(local_errors.filtered_errors()) results.append(result) else: - if isinstance(obj, Instance) and isinstance( - defn := obj.type.get_method(name), OverloadedFuncDef - ): - for item in defn.items: - if ( - isinstance(item, Decorator) - and isinstance(typ := item.func.type, CallableType) - and bind_self(typ) == result[1] - ): - self.chk.check_deprecated(item.func, context) return result # We finish invoking above operators and no early return happens. Therefore, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 7eedab2e399a..8447dfc7fe64 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from typing import Callable, TypeVar, cast -from mypy import message_registry, state, subtypes +from mypy import message_registry, state from mypy.checker_shared import TypeCheckerSharedApi from mypy.erasetype import erase_typevars from mypy.expandtype import ( @@ -14,6 +14,7 @@ freshen_all_functions_type_vars, ) from mypy.maptype import map_instance_to_supertype +from mypy.meet import is_overlapping_types from mypy.messages import MessageBuilder from mypy.nodes import ( ARG_POS, @@ -39,6 +40,7 @@ is_final_node, ) from mypy.plugin import AttributeContext +from mypy.subtypes import is_subtype from mypy.typeops import ( bind_self, erase_to_bound, @@ -745,10 +747,8 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: callable_name=callable_name, ) - mx.chk.check_deprecated(dunder_get, mx.context) - mx.chk.warn_deprecated_overload_item( - dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type - ) + # Search for possible deprecations: + mx.chk.warn_deprecated(dunder_get, mx.context) inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type) if isinstance(inferred_dunder_get_type, AnyType): @@ -825,10 +825,7 @@ def analyze_descriptor_assign(descriptor_type: Instance, mx: MemberContext) -> T ) # Search for possible deprecations: - mx.chk.check_deprecated(dunder_set, mx.context) - mx.chk.warn_deprecated_overload_item( - dunder_set, mx.context, target=inferred_dunder_set_type, selftype=descriptor_type - ) + mx.chk.warn_deprecated(dunder_set, mx.context) # In the following cases, a message already will have been recorded in check_call. if (not isinstance(inferred_dunder_set_type, CallableType)) or ( @@ -1053,6 +1050,7 @@ def f(self: S) -> T: ... new_items = [] if is_classmethod: dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type) + p_dispatched_arg_type = get_proper_type(dispatched_arg_type) for item in items: if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): @@ -1061,28 +1059,42 @@ def f(self: S) -> T: ... # This is pretty bad, so just return the original signature if # there is at least one such error. return functype - else: - selfarg = get_proper_type(item.arg_types[0]) - # This matches similar special-casing in bind_self(), see more details there. - self_callable = name == "__call__" and isinstance(selfarg, CallableType) - if self_callable or subtypes.is_subtype( - dispatched_arg_type, - # This level of erasure matches the one in checker.check_func_def(), - # better keep these two checks consistent. - erase_typevars(erase_to_bound(selfarg)), - # This is to work around the fact that erased ParamSpec and TypeVarTuple - # callables are not always compatible with non-erased ones both ways. - always_covariant=any( - not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg) - ), - ignore_pos_arg_names=True, - ): - new_items.append(item) - elif isinstance(selfarg, ParamSpecType): - # TODO: This is not always right. What's the most reasonable thing to do here? - new_items.append(item) - elif isinstance(selfarg, TypeVarTupleType): - raise NotImplementedError + selfarg = get_proper_type(item.arg_types[0]) + if isinstance(selfarg, Instance) and isinstance(p_dispatched_arg_type, Instance): + if selfarg.type is p_dispatched_arg_type.type and selfarg.args: + if not is_overlapping_types(p_dispatched_arg_type, selfarg): + # This special casing is needed since `actual <: erased(template)` + # logic below doesn't always work, and a more correct approach may + # be tricky. + continue + new_items.append(item) + + if new_items: + items = new_items + new_items = [] + + for item in items: + selfarg = get_proper_type(item.arg_types[0]) + # This matches similar special-casing in bind_self(), see more details there. + self_callable = name == "__call__" and isinstance(selfarg, CallableType) + if self_callable or is_subtype( + dispatched_arg_type, + # This level of erasure matches the one in checker.check_func_def(), + # better keep these two checks consistent. + erase_typevars(erase_to_bound(selfarg)), + # This is to work around the fact that erased ParamSpec and TypeVarTuple + # callables are not always compatible with non-erased ones both ways. + always_covariant=any( + not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg) + ), + ignore_pos_arg_names=True, + ): + new_items.append(item) + elif isinstance(selfarg, ParamSpecType): + # TODO: This is not always right. What's the most reasonable thing to do here? + new_items.append(item) + elif isinstance(selfarg, TypeVarTupleType): + raise NotImplementedError if not new_items: # Choose first item for the message (it may be not very helpful for overloads). msg.incompatible_self_argument( diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 6c47670d6687..3f33ea1648f0 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -145,29 +145,34 @@ def erase_typevars(t: Type, ids_to_erase: Container[TypeVarId] | None = None) -> or just the ones in the provided collection. """ + if ids_to_erase is None: + return t.accept(TypeVarEraser(None, AnyType(TypeOfAny.special_form))) + def erase_id(id: TypeVarId) -> bool: - if ids_to_erase is None: - return True return id in ids_to_erase return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form))) +def erase_meta_id(id: TypeVarId) -> bool: + return id.is_meta_var() + + def replace_meta_vars(t: Type, target_type: Type) -> Type: """Replace unification variables in a type with the target type.""" - return t.accept(TypeVarEraser(lambda id: id.is_meta_var(), target_type)) + return t.accept(TypeVarEraser(erase_meta_id, target_type)) class TypeVarEraser(TypeTranslator): """Implementation of type erasure""" - def __init__(self, erase_id: Callable[[TypeVarId], bool], replacement: Type) -> None: + def __init__(self, erase_id: Callable[[TypeVarId], bool] | None, replacement: Type) -> None: super().__init__() self.erase_id = erase_id self.replacement = replacement def visit_type_var(self, t: TypeVarType) -> Type: - if self.erase_id(t.id): + if self.erase_id is None or self.erase_id(t.id): return self.replacement return t @@ -212,12 +217,12 @@ def visit_callable_type(self, t: CallableType) -> Type: return result def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: - if self.erase_id(t.id): + if self.erase_id is None or self.erase_id(t.id): return t.tuple_fallback.copy_modified(args=[self.replacement]) return t def visit_param_spec(self, t: ParamSpecType) -> Type: - if self.erase_id(t.id): + if self.erase_id is None or self.erase_id(t.id): return self.replacement return t diff --git a/mypy/fixup.py b/mypy/fixup.py index 0e9c186fd42a..c0f8e401777c 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -165,6 +165,8 @@ def visit_func_def(self, func: FuncDef) -> None: func.info = self.current_info if func.type is not None: func.type.accept(self.type_fixer) + if isinstance(func.type, CallableType): + func.type.definition = func def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None: if self.current_info is not None: diff --git a/mypy/meet.py b/mypy/meet.py index 2e238be7765e..28bf937ddbaf 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -291,6 +291,18 @@ def is_object(t: ProperType) -> bool: return isinstance(t, Instance) and t.type.fullname == "builtins.object" +def is_none_typevarlike_overlap(t1: ProperType, t2: ProperType) -> bool: + return isinstance(t1, NoneType) and isinstance(t2, TypeVarLikeType) + + +def is_none_object_overlap(t1: ProperType, t2: ProperType) -> bool: + return ( + isinstance(t1, NoneType) + and isinstance(t2, Instance) + and t2.type.fullname == "builtins.object" + ) + + def is_overlapping_types( left: Type, right: Type, @@ -382,14 +394,6 @@ def _is_overlapping_types(left: Type, right: Type) -> bool: ): return True - def is_none_object_overlap(t1: Type, t2: Type) -> bool: - t1, t2 = get_proper_types((t1, t2)) - return ( - isinstance(t1, NoneType) - and isinstance(t2, Instance) - and t2.type.fullname == "builtins.object" - ) - if overlap_for_overloads: if is_none_object_overlap(left, right) or is_none_object_overlap(right, left): return False @@ -419,10 +423,6 @@ def _is_subtype(left: Type, right: Type) -> bool: # If both types are singleton variants (and are not TypeVarLikes), we've hit the base case: # we skip these checks to avoid infinitely recursing. - def is_none_typevarlike_overlap(t1: Type, t2: Type) -> bool: - t1, t2 = get_proper_types((t1, t2)) - return isinstance(t1, NoneType) and isinstance(t2, TypeVarLikeType) - if prohibit_none_typevar_overlap: if is_none_typevarlike_overlap(left, right) or is_none_typevarlike_overlap(right, left): return False diff --git a/mypy/nodes.py b/mypy/nodes.py index 921620866a06..011e4e703a0c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -595,12 +595,14 @@ def is_trivial_self(self) -> bool: """ if self._is_trivial_self is not None: return self._is_trivial_self - for item in self.items: + for i, item in enumerate(self.items): + # Note: bare @property is removed in visit_decorator(). + trivial = 1 if i > 0 or not self.is_property else 0 if isinstance(item, FuncDef): if not item.is_trivial_self: self._is_trivial_self = False return False - elif item.decorators or not item.func.is_trivial_self: + elif len(item.decorators) > trivial or not item.func.is_trivial_self: self._is_trivial_self = False return False self._is_trivial_self = True diff --git a/mypy/typeops.py b/mypy/typeops.py index 1c22a1711944..75213bd93674 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -16,7 +16,6 @@ from mypy.expandtype import expand_type, expand_type_by_instance from mypy.maptype import map_instance_to_supertype from mypy.nodes import ( - ARG_OPT, ARG_POS, ARG_STAR, ARG_STAR2, @@ -421,27 +420,9 @@ class B(A): pass """ if isinstance(method, Overloaded): - items = [] - original_type = get_proper_type(original_type) - for c in method.items: - if isinstance(original_type, Instance): - # Filter based on whether declared self type can match actual object type. - # For example, if self has type C[int] and method is accessed on a C[str] value, - # omit this item. This is best effort since bind_self can be called in many - # contexts, and doing complete validation might trigger infinite recursion. - # - # Note that overload item filtering normally happens elsewhere. This is needed - # at least during constraint inference. - keep = is_valid_self_type_best_effort(c, original_type) - else: - keep = True - if keep: - items.append(bind_self(c, original_type, is_classmethod, ignore_instances)) - if len(items) == 0: - # If no item matches, returning all items helps avoid some spurious errors - items = [ - bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items - ] + items = [ + bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items + ] return cast(F, Overloaded(items)) assert isinstance(method, CallableType) func: CallableType = method @@ -510,43 +491,6 @@ class B(A): pass return cast(F, res) -def is_valid_self_type_best_effort(c: CallableType, self_type: Instance) -> bool: - """Quickly check if self_type might match the self in a callable. - - Avoid performing any complex type operations. This is performance-critical. - - Default to returning True if we don't know (or it would be too expensive). - """ - if ( - self_type.args - and c.arg_types - and isinstance((arg_type := get_proper_type(c.arg_types[0])), Instance) - and c.arg_kinds[0] in (ARG_POS, ARG_OPT) - and arg_type.args - and self_type.type.fullname != "functools._SingleDispatchCallable" - ): - if self_type.type is not arg_type.type: - # We can't map to supertype, since it could trigger expensive checks for - # protocol types, so we consevatively assume this is fine. - return True - - # Fast path: no explicit annotation on self - if all( - ( - type(arg) is TypeVarType - and type(arg.upper_bound) is Instance - and arg.upper_bound.type.fullname == "builtins.object" - ) - for arg in arg_type.args - ): - return True - - from mypy.meet import is_overlapping_types - - return is_overlapping_types(self_type, c.arg_types[0]) - return True - - def erase_to_bound(t: Type) -> Type: # TODO: use value restrictions to produce a union? t = get_proper_type(t) diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 05c34eb70796..d99c5ee354a4 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -2282,3 +2282,30 @@ class Check: reveal_type(Check.foo()) # N: Revealed type is "def () -> __main__.Check" reveal_type(Check().foo()) # N: Revealed type is "__main__.Check" [builtins fixtures/tuple.pyi] + +[case testSelfTypeUpperBoundFiler] +from typing import Generic, TypeVar, overload, Sequence + +class B: ... +class C(B): ... + +TB = TypeVar("TB", bound=B) +TC = TypeVar("TC", bound=C) + +class G(Generic[TB]): + @overload + def test(self: G[TC]) -> list[TC]: ... + @overload + def test(self: G[TB]) -> Sequence[TB]: ... + def test(self): + ... + +class D1(B): ... +class D2(C): ... + +gb: G[D1] +gc: G[D2] + +reveal_type(gb.test()) # N: Revealed type is "typing.Sequence[__main__.D1]" +reveal_type(gc.test()) # N: Revealed type is "builtins.list[__main__.D2]" +[builtins fixtures/list.pyi] diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index 63d9ccfc80cb..5265832f5f27 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -158,6 +158,7 @@ def f(__x: int) -> None: pass [out2] tmp/a.py:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" tmp/a.py:4: error: Unexpected keyword argument "__x" for "f" +tmp/b.py: note: "f" defined here [case testSerializeArgumentKindsErrors] import a