Skip to content
Merged
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
27 changes: 24 additions & 3 deletions mypy/checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -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."""
Expand Down
6 changes: 2 additions & 4 deletions mypy/checkexpr.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
10 changes: 9 additions & 1 deletion mypy/checkmember.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand DownExpand Up@@ -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
Expand Down
78 changes: 78 additions & 0 deletions test-data/unit/check-deprecated.test
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand DownExpand Up@@ -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]