Skip to content

Commit

Permalink
PEP 702 (@deprecated): descriptors (#18090)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyralla authored and svalentin committed Dec 17, 2024
1 parent 5082a22 commit e6ce8be
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 8 deletions.
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]

0 comments on commit e6ce8be

Please sign in to comment.