Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 702 (@deprecated): descriptors #18090

Merged
merged 9 commits into from
Dec 6, 2024
27 changes: 24 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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]
Loading