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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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 number Diff line number Diff line change
Expand Up @@ -4425,7 +4425,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 @@ -4492,6 +4492,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 @@ -7663,7 +7669,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 @@ -7676,7 +7682,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 @@ -7688,6 +7694,21 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None:
warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note
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 @@ -1482,10 +1482,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):
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
53 changes: 53 additions & 0 deletions test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,59 @@ C().g = "x" # N: function __main__.C.g is deprecated: use g2 instead \
[builtins fixtures/property.pyi]


[case testDeprecatedDescriptor]

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() # N: class __main__.D1 is deprecated: use E1 instead
d2 = D2()
d3 = D3()

c: C
C.d1
c.d1
c.d1 = 1

C.d2 # N: function __main__.D2.__get__ is deprecated: use E2.__get__ instead
c.d2 # N: function __main__.D2.__get__ is deprecated: use E2.__get__ instead
c.d2 = 1 # N: function __main__.D2.__set__ is deprecated: use E2.__set__ instead

C.d3 # N: overload def (self: __main__.D3, obj: None, objtype: Any) -> __main__.D3 of function __main__.D3.__get__ is deprecated: use E3.__get__ instead
c.d3 # N: 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" # N: 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]

from typing import Union
Expand Down
Loading