diff --git a/mypy/checker.py b/mypy/checker.py index 379da3f1c0da..2edcaa6bc5c5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4426,7 +4426,7 @@ def check_member_assignment( msg=self.msg, chk=self, ) - get_type = analyze_descriptor_access(attribute_type, mx) + get_type = analyze_descriptor_access(attribute_type, mx, assignment=True) if not attribute_type.type.has_readable_member("__set__"): # If there is no __set__, we type-check that the assigned value matches # the return type of __get__. This doesn't match the python semantics, @@ -4493,6 +4493,12 @@ def check_member_assignment( callable_name=callable_name, ) + # 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=attribute_type + ) + # In the following cases, a message already will have been recorded in check_call. if (not isinstance(inferred_dunder_set_type, CallableType)) or ( len(inferred_dunder_set_type.arg_types) < 2 @@ -7674,7 +7680,7 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) - def check_deprecated(self, node: SymbolNode | None, context: Context) -> None: + def check_deprecated(self, node: Node | None, context: Context) -> None: """Warn if deprecated and not directly imported with a `from` statement.""" if isinstance(node, Decorator): node = node.func @@ -7687,7 +7693,7 @@ def check_deprecated(self, node: SymbolNode | None, context: Context) -> None: else: self.warn_deprecated(node, context) - def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: + def warn_deprecated(self, node: Node | None, context: Context) -> None: """Warn if deprecated.""" if isinstance(node, Decorator): node = node.func @@ -7699,6 +7705,21 @@ def warn_deprecated(self, node: SymbolNode | 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: + candidate = bind_self(candidate, selftype) + if candidate == target: + self.warn_deprecated(item.func, context) + class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 549026ca89c2..34004b5e3b48 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1483,10 +1483,8 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) - if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, OverloadedFuncDef): - for item in e.callee.node.items: - if isinstance(item, Decorator) and (item.func.type == callee_type): - self.chk.check_deprecated(item.func, e) + if isinstance(e.callee, (NameExpr, MemberExpr)): + self.chk.warn_deprecated_overload_item(e.callee.node, e, target=callee_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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 9dc8d5475b1a..50e54ca30460 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -638,7 +638,9 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx) -def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: +def analyze_descriptor_access( + descriptor_type: Type, mx: MemberContext, *, assignment: bool = False +) -> Type: """Type check descriptor access. Arguments: @@ -719,6 +721,12 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: callable_name=callable_name, ) + if not assignment: + 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 + ) + inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type) if isinstance(inferred_dunder_get_type, AnyType): # check_call failed, and will have reported an error diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 8bbb887d4567..362d8725f183 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -503,6 +503,60 @@ C().g = "x" # E: function __main__.C.g is deprecated: use g2 instead \ [builtins fixtures/property.pyi] +[case testDeprecatedDescriptor] +# flags: --enable-error-code=deprecated + +from typing import Any, Optional, Union +from typing_extensions import deprecated, overload + +@deprecated("use E1 instead") +class D1: + def __get__(self, obj: Optional[C], objtype: Any) -> Union[D1, int]: ... + +class D2: + @deprecated("use E2.__get__ instead") + def __get__(self, obj: Optional[C], objtype: Any) -> Union[D2, int]: ... + + @deprecated("use E2.__set__ instead") + def __set__(self, obj: C, value: int) -> None: ... + +class D3: + @overload + @deprecated("use E3.__get__ instead") + def __get__(self, obj: None, objtype: Any) -> D3: ... + @overload + @deprecated("use E3.__get__ instead") + def __get__(self, obj: C, objtype: Any) -> int: ... + def __get__(self, obj: Optional[C], objtype: Any) -> Union[D3, int]: ... + + @overload + def __set__(self, obj: C, value: int) -> None: ... + @overload + @deprecated("use E3.__set__ instead") + def __set__(self, obj: C, value: str) -> None: ... + def __set__(self, obj: C, value: Union[int, str]) -> None: ... + +class C: + d1 = D1() # E: class __main__.D1 is deprecated: use E1 instead + d2 = D2() + d3 = D3() + +c: C +C.d1 +c.d1 +c.d1 = 1 + +C.d2 # E: function __main__.D2.__get__ is deprecated: use E2.__get__ instead +c.d2 # E: function __main__.D2.__get__ is deprecated: use E2.__get__ instead +c.d2 = 1 # E: function __main__.D2.__set__ is deprecated: use E2.__set__ instead + +C.d3 # E: overload def (self: __main__.D3, obj: None, objtype: Any) -> __main__.D3 of function __main__.D3.__get__ is deprecated: use E3.__get__ instead +c.d3 # E: overload def (self: __main__.D3, obj: __main__.C, objtype: Any) -> builtins.int of function __main__.D3.__get__ is deprecated: use E3.__get__ instead +c.d3 = 1 +c.d3 = "x" # E: overload def (self: __main__.D3, obj: __main__.C, value: builtins.str) of function __main__.D3.__set__ is deprecated: use E3.__set__ instead +[builtins fixtures/property.pyi] + + [case testDeprecatedOverloadedFunction] # flags: --enable-error-code=deprecated @@ -556,3 +610,27 @@ h(1.0) # E: No overload variant of "h" matches argument type "float" \ # N: def h(x: str) -> str [builtins fixtures/tuple.pyi] + + +[case testDeprecatedImportedOverloadedFunction] +# flags: --enable-error-code=deprecated + +import m + +m.g +m.g(1) # E: overload def (x: builtins.int) -> builtins.int of function m.g is deprecated: work with str instead +m.g("x") + +[file m.py] + +from typing import Union +from typing_extensions import deprecated, overload + +@overload +@deprecated("work with str instead") +def g(x: int) -> int: ... +@overload +def g(x: str) -> str: ... +def g(x: Union[int, str]) -> Union[int, str]: ... + +[builtins fixtures/tuple.pyi]