Skip to content

Commit e6ce8be

Browse files
tyrallasvalentin
authored andcommitted
PEP 702 (@deprecated): descriptors (#18090)
1 parent 5082a22 commit e6ce8be

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

mypy/checker.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -4426,7 +4426,7 @@ def check_member_assignment(
44264426
msg=self.msg,
44274427
chk=self,
44284428
)
4429-
get_type = analyze_descriptor_access(attribute_type, mx)
4429+
get_type = analyze_descriptor_access(attribute_type, mx, assignment=True)
44304430
if not attribute_type.type.has_readable_member("__set__"):
44314431
# If there is no __set__, we type-check that the assigned value matches
44324432
# the return type of __get__. This doesn't match the python semantics,
@@ -4493,6 +4493,12 @@ def check_member_assignment(
44934493
callable_name=callable_name,
44944494
)
44954495

4496+
# Search for possible deprecations:
4497+
mx.chk.check_deprecated(dunder_set, mx.context)
4498+
mx.chk.warn_deprecated_overload_item(
4499+
dunder_set, mx.context, target=inferred_dunder_set_type, selftype=attribute_type
4500+
)
4501+
44964502
# In the following cases, a message already will have been recorded in check_call.
44974503
if (not isinstance(inferred_dunder_set_type, CallableType)) or (
44984504
len(inferred_dunder_set_type.arg_types) < 2
@@ -7674,7 +7680,7 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool:
76747680
def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type:
76757681
return self.expr_checker.accept(node, type_context=type_context)
76767682

7677-
def check_deprecated(self, node: SymbolNode | None, context: Context) -> None:
7683+
def check_deprecated(self, node: Node | None, context: Context) -> None:
76787684
"""Warn if deprecated and not directly imported with a `from` statement."""
76797685
if isinstance(node, Decorator):
76807686
node = node.func
@@ -7687,7 +7693,7 @@ def check_deprecated(self, node: SymbolNode | None, context: Context) -> None:
76877693
else:
76887694
self.warn_deprecated(node, context)
76897695

7690-
def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None:
7696+
def warn_deprecated(self, node: Node | None, context: Context) -> None:
76917697
"""Warn if deprecated."""
76927698
if isinstance(node, Decorator):
76937699
node = node.func
@@ -7699,6 +7705,21 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None:
76997705
warn = self.msg.note if self.options.report_deprecated_as_note else self.msg.fail
77007706
warn(deprecated, context, code=codes.DEPRECATED)
77017707

7708+
def warn_deprecated_overload_item(
7709+
self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None
7710+
) -> None:
7711+
"""Warn if the overload item corresponding to the given callable is deprecated."""
7712+
target = get_proper_type(target)
7713+
if isinstance(node, OverloadedFuncDef) and isinstance(target, CallableType):
7714+
for item in node.items:
7715+
if isinstance(item, Decorator) and isinstance(
7716+
candidate := item.func.type, CallableType
7717+
):
7718+
if selftype is not None:
7719+
candidate = bind_self(candidate, selftype)
7720+
if candidate == target:
7721+
self.warn_deprecated(item.func, context)
7722+
77027723

77037724
class CollectArgTypeVarTypes(TypeTraverserVisitor):
77047725
"""Collects the non-nested argument types in a set."""

mypy/checkexpr.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1483,10 +1483,8 @@ def check_call_expr_with_callee_type(
14831483
object_type=object_type,
14841484
)
14851485
proper_callee = get_proper_type(callee_type)
1486-
if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, OverloadedFuncDef):
1487-
for item in e.callee.node.items:
1488-
if isinstance(item, Decorator) and (item.func.type == callee_type):
1489-
self.chk.check_deprecated(item.func, e)
1486+
if isinstance(e.callee, (NameExpr, MemberExpr)):
1487+
self.chk.warn_deprecated_overload_item(e.callee.node, e, target=callee_type)
14901488
if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType):
14911489
# Cache it for find_isinstance_check()
14921490
if proper_callee.type_guard is not None:

mypy/checkmember.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,9 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont
638638
msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx)
639639

640640

641-
def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
641+
def analyze_descriptor_access(
642+
descriptor_type: Type, mx: MemberContext, *, assignment: bool = False
643+
) -> Type:
642644
"""Type check descriptor access.
643645
644646
Arguments:
@@ -719,6 +721,12 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
719721
callable_name=callable_name,
720722
)
721723

724+
if not assignment:
725+
mx.chk.check_deprecated(dunder_get, mx.context)
726+
mx.chk.warn_deprecated_overload_item(
727+
dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type
728+
)
729+
722730
inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type)
723731
if isinstance(inferred_dunder_get_type, AnyType):
724732
# check_call failed, and will have reported an error

test-data/unit/check-deprecated.test

+78
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,60 @@ C().g = "x" # E: function __main__.C.g is deprecated: use g2 instead \
503503
[builtins fixtures/property.pyi]
504504

505505

506+
[case testDeprecatedDescriptor]
507+
# flags: --enable-error-code=deprecated
508+
509+
from typing import Any, Optional, Union
510+
from typing_extensions import deprecated, overload
511+
512+
@deprecated("use E1 instead")
513+
class D1:
514+
def __get__(self, obj: Optional[C], objtype: Any) -> Union[D1, int]: ...
515+
516+
class D2:
517+
@deprecated("use E2.__get__ instead")
518+
def __get__(self, obj: Optional[C], objtype: Any) -> Union[D2, int]: ...
519+
520+
@deprecated("use E2.__set__ instead")
521+
def __set__(self, obj: C, value: int) -> None: ...
522+
523+
class D3:
524+
@overload
525+
@deprecated("use E3.__get__ instead")
526+
def __get__(self, obj: None, objtype: Any) -> D3: ...
527+
@overload
528+
@deprecated("use E3.__get__ instead")
529+
def __get__(self, obj: C, objtype: Any) -> int: ...
530+
def __get__(self, obj: Optional[C], objtype: Any) -> Union[D3, int]: ...
531+
532+
@overload
533+
def __set__(self, obj: C, value: int) -> None: ...
534+
@overload
535+
@deprecated("use E3.__set__ instead")
536+
def __set__(self, obj: C, value: str) -> None: ...
537+
def __set__(self, obj: C, value: Union[int, str]) -> None: ...
538+
539+
class C:
540+
d1 = D1() # E: class __main__.D1 is deprecated: use E1 instead
541+
d2 = D2()
542+
d3 = D3()
543+
544+
c: C
545+
C.d1
546+
c.d1
547+
c.d1 = 1
548+
549+
C.d2 # E: function __main__.D2.__get__ is deprecated: use E2.__get__ instead
550+
c.d2 # E: function __main__.D2.__get__ is deprecated: use E2.__get__ instead
551+
c.d2 = 1 # E: function __main__.D2.__set__ is deprecated: use E2.__set__ instead
552+
553+
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
554+
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
555+
c.d3 = 1
556+
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
557+
[builtins fixtures/property.pyi]
558+
559+
506560
[case testDeprecatedOverloadedFunction]
507561
# flags: --enable-error-code=deprecated
508562

@@ -556,3 +610,27 @@ h(1.0) # E: No overload variant of "h" matches argument type "float" \
556610
# N: def h(x: str) -> str
557611

558612
[builtins fixtures/tuple.pyi]
613+
614+
615+
[case testDeprecatedImportedOverloadedFunction]
616+
# flags: --enable-error-code=deprecated
617+
618+
import m
619+
620+
m.g
621+
m.g(1) # E: overload def (x: builtins.int) -> builtins.int of function m.g is deprecated: work with str instead
622+
m.g("x")
623+
624+
[file m.py]
625+
626+
from typing import Union
627+
from typing_extensions import deprecated, overload
628+
629+
@overload
630+
@deprecated("work with str instead")
631+
def g(x: int) -> int: ...
632+
@overload
633+
def g(x: str) -> str: ...
634+
def g(x: Union[int, str]) -> Union[int, str]: ...
635+
636+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)