Skip to content

Commit 0c3bd81

Browse files
committed
PEP 702 (@deprecated): improve the handling of overloaded functions and methods
1 parent 8104d01 commit 0c3bd81

File tree

6 files changed

+368
-111
lines changed

6 files changed

+368
-111
lines changed

mypy/checker.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4648,9 +4648,6 @@ def check_member_assignment(
46484648

46494649
# Search for possible deprecations:
46504650
mx.chk.check_deprecated(dunder_set, mx.context)
4651-
mx.chk.warn_deprecated_overload_item(
4652-
dunder_set, mx.context, target=inferred_dunder_set_type, selftype=attribute_type
4653-
)
46544651

46554652
# In the following cases, a message already will have been recorded in check_call.
46564653
if (not isinstance(inferred_dunder_set_type, CallableType)) or (
@@ -7894,21 +7891,6 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None:
78947891
warn = self.msg.note if self.options.report_deprecated_as_note else self.msg.fail
78957892
warn(deprecated, context, code=codes.DEPRECATED)
78967893

7897-
def warn_deprecated_overload_item(
7898-
self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None
7899-
) -> None:
7900-
"""Warn if the overload item corresponding to the given callable is deprecated."""
7901-
target = get_proper_type(target)
7902-
if isinstance(node, OverloadedFuncDef) and isinstance(target, CallableType):
7903-
for item in node.items:
7904-
if isinstance(item, Decorator) and isinstance(
7905-
candidate := item.func.type, CallableType
7906-
):
7907-
if selftype is not None and not node.is_static:
7908-
candidate = bind_self(candidate, selftype)
7909-
if candidate == target:
7910-
self.warn_deprecated(item.func, context)
7911-
79127894
# leafs
79137895

79147896
def visit_pass_stmt(self, o: PassStmt, /) -> None:

mypy/checkexpr.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,15 +1476,6 @@ def check_call_expr_with_callee_type(
14761476
object_type=object_type,
14771477
)
14781478
proper_callee = get_proper_type(callee_type)
1479-
if isinstance(e.callee, (NameExpr, MemberExpr)):
1480-
node = e.callee.node
1481-
if node is None and member is not None and isinstance(object_type, Instance):
1482-
if (symbol := object_type.type.get(member)) is not None:
1483-
node = symbol.node
1484-
self.chk.check_deprecated(node, e)
1485-
self.chk.warn_deprecated_overload_item(
1486-
node, e, target=callee_type, selftype=object_type
1487-
)
14881479
if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType):
14891480
# Cache it for find_isinstance_check()
14901481
if proper_callee.type_guard is not None:
@@ -2652,6 +2643,25 @@ def check_overload_call(
26522643
context: Context,
26532644
) -> tuple[Type, Type]:
26542645
"""Checks a call to an overloaded function."""
2646+
2647+
# The following hack tries to update the `definition` attribute of the given callable
2648+
# type's items with the related decorator symbols to allow checking for deprecations:
2649+
funcdef = None
2650+
if callable_name is not None:
2651+
if isinstance(inst := get_proper_type(object_type), Instance):
2652+
if (sym := inst.type.get(callable_name.rpartition(".")[-1])) is not None:
2653+
funcdef = sym.node
2654+
else:
2655+
name_module, _, name = callable_name.rpartition(".")
2656+
if (
2657+
(module := self.chk.modules.get(name_module)) is not None
2658+
and (sym := module.names.get(name)) is not None
2659+
):
2660+
funcdef = sym.node
2661+
if isinstance(funcdef, OverloadedFuncDef):
2662+
for typ, defn in zip(callee.items, funcdef.items):
2663+
typ.definition = defn
2664+
26552665
# Normalize unpacked kwargs before checking the call.
26562666
callee = callee.with_unpacked_kwargs()
26572667
arg_types = self.infer_arg_types_in_empty_context(args)
@@ -2714,19 +2724,25 @@ def check_overload_call(
27142724
object_type,
27152725
context,
27162726
)
2717-
# If any of checks succeed, stop early.
2727+
# If any of checks succeed, perform deprecation tests and stop early.
27182728
if inferred_result is not None and unioned_result is not None:
27192729
# Both unioned and direct checks succeeded, choose the more precise type.
27202730
if (
27212731
is_subtype(inferred_result[0], unioned_result[0])
27222732
and not isinstance(get_proper_type(inferred_result[0]), AnyType)
27232733
and not none_type_var_overlap
27242734
):
2725-
return inferred_result
2726-
return unioned_result
2727-
elif unioned_result is not None:
2735+
unioned_result = None
2736+
else:
2737+
inferred_result = None
2738+
if unioned_result is not None:
2739+
for inferred_type in inferred_types:
2740+
if isinstance(c := get_proper_type(inferred_type), CallableType):
2741+
self.chk.warn_deprecated(c.definition, context)
27282742
return unioned_result
2729-
elif inferred_result is not None:
2743+
if inferred_result is not None:
2744+
if isinstance(c := get_proper_type(inferred_result[1]), CallableType):
2745+
self.chk.warn_deprecated(c.definition, context)
27302746
return inferred_result
27312747

27322748
# Step 4: Failure. At this point, we know there is no match. We fall back to trying
@@ -4077,21 +4093,11 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
40774093
results = []
40784094
for name, method, obj, arg in variants:
40794095
with self.msg.filter_errors(save_filtered_errors=True) as local_errors:
4080-
result = self.check_method_call(op_name, obj, method, [arg], [ARG_POS], context)
4096+
result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context)
40814097
if local_errors.has_new_errors():
40824098
errors.append(local_errors.filtered_errors())
40834099
results.append(result)
40844100
else:
4085-
if isinstance(obj, Instance) and isinstance(
4086-
defn := obj.type.get_method(name), OverloadedFuncDef
4087-
):
4088-
for item in defn.items:
4089-
if (
4090-
isinstance(item, Decorator)
4091-
and isinstance(typ := item.func.type, CallableType)
4092-
and bind_self(typ) == result[1]
4093-
):
4094-
self.chk.check_deprecated(item.func, context)
40954101
return result
40964102

40974103
# We finish invoking above operators and no early return happens. Therefore,

mypy/checkmember.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from mypy import message_registry, subtypes
99
from mypy.erasetype import erase_typevars
10+
import mypy.errorcodes as codes
1011
from mypy.expandtype import (
1112
expand_self_type,
1213
expand_type_by_instance,
@@ -701,6 +702,10 @@ def analyze_descriptor_access(
701702
object_type=descriptor_type,
702703
)
703704

705+
deprecated_disabled = False
706+
if assignment and codes.DEPRECATED in mx.chk.options.enabled_error_codes:
707+
mx.chk.options.enabled_error_codes.remove(codes.DEPRECATED)
708+
deprecated_disabled = True
704709
_, inferred_dunder_get_type = mx.chk.expr_checker.check_call(
705710
dunder_get_type,
706711
[
@@ -712,12 +717,10 @@ def analyze_descriptor_access(
712717
object_type=descriptor_type,
713718
callable_name=callable_name,
714719
)
715-
720+
if deprecated_disabled:
721+
mx.chk.options.enabled_error_codes.add(codes.DEPRECATED)
716722
if not assignment:
717723
mx.chk.check_deprecated(dunder_get, mx.context)
718-
mx.chk.warn_deprecated_overload_item(
719-
dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type
720-
)
721724

722725
inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type)
723726
if isinstance(inferred_dunder_get_type, AnyType):

mypy/server/astdiff.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
252252
setter_type = snapshot_optional_type(first_item.var.setter_type)
253253
is_trivial_body = impl.is_trivial_body if impl else False
254254
dataclass_transform_spec = find_dataclass_transform_spec(node)
255+
256+
deprecated = None
257+
if isinstance(node, FuncDef):
258+
deprecated = node.deprecated
259+
elif isinstance(node, OverloadedFuncDef):
260+
deprecated_list = [node.deprecated] + [i.func.deprecated for i in node.items]
261+
deprecated_list_cleaned = [d for d in deprecated_list if d is not None]
262+
if deprecated_list_cleaned:
263+
deprecated = ",".join(deprecated_list_cleaned)
264+
255265
return (
256266
"Func",
257267
common,
@@ -262,7 +272,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
262272
signature,
263273
is_trivial_body,
264274
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
265-
node.deprecated if isinstance(node, FuncDef) else None,
275+
deprecated,
266276
setter_type, # multi-part properties are stored as OverloadedFuncDef
267277
)
268278
elif isinstance(node, Var):

test-data/unit/check-deprecated.test

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,8 @@ b.h("x") # E: function __main__.A.h is deprecated: use `h2` instead
429429
[case testDeprecatedOverloadedClassMethods]
430430
# flags: --enable-error-code=deprecated
431431

432-
from typing import Iterator, Union, overload
433-
from typing_extensions import deprecated
432+
from typing import Iterator, Union
433+
from typing_extensions import deprecated, overload
434434

435435
class A:
436436
@overload
@@ -487,8 +487,8 @@ b.h("x") # E: function __main__.A.h is deprecated: use `h2` instead
487487
[case testDeprecatedOverloadedStaticMethods]
488488
# flags: --enable-error-code=deprecated
489489

490-
from typing import Iterator, Union, overload
491-
from typing_extensions import deprecated
490+
from typing import Iterator, Union
491+
from typing_extensions import deprecated, overload
492492

493493
class A:
494494
@overload
@@ -545,8 +545,8 @@ b.h("x") # E: function __main__.A.h is deprecated: use `h2` instead
545545
[case testDeprecatedOverloadedSpecialMethods]
546546
# flags: --enable-error-code=deprecated
547547

548-
from typing import Iterator, Union, overload
549-
from typing_extensions import deprecated
548+
from typing import Iterator, Union
549+
from typing_extensions import deprecated, overload
550550

551551
class A:
552552
@overload
@@ -671,8 +671,8 @@ C().g = "x" # E: function __main__.C.g is deprecated: use g2 instead \
671671
[case testDeprecatedDescriptor]
672672
# flags: --enable-error-code=deprecated
673673

674-
from typing import Any, Optional, Union, overload
675-
from typing_extensions import deprecated
674+
from typing import Any, Optional, Union
675+
from typing_extensions import deprecated, overload
676676

677677
@deprecated("use E1 instead")
678678
class D1:
@@ -725,8 +725,8 @@ c.d3 = "x" # E: overload def (self: __main__.D3, obj: __main__.C, value: builti
725725
[case testDeprecatedOverloadedFunction]
726726
# flags: --enable-error-code=deprecated
727727

728-
from typing import Union, overload
729-
from typing_extensions import deprecated
728+
from typing import Union
729+
from typing_extensions import deprecated, overload
730730

731731
@overload
732732
def f(x: int) -> int: ...
@@ -788,65 +788,14 @@ m.g("x")
788788

789789
[file m.py]
790790

791-
from typing import Union, overload
792-
from typing_extensions import deprecated
791+
from typing import Union
792+
from typing_extensions import deprecated, overload
793793

794794
@overload
795795
@deprecated("work with str instead")
796796
def g(x: int) -> int: ...
797797
@overload
798798
def g(x: str) -> str: ...
799799
def g(x: Union[int, str]) -> Union[int, str]: ...
800-
[builtins fixtures/tuple.pyi]
801-
802-
[case testDeprecatedExclude]
803-
# flags: --enable-error-code=deprecated --deprecated-calls-exclude=m.C --deprecated-calls-exclude=m.D --deprecated-calls-exclude=m.E.f --deprecated-calls-exclude=m.E.g --deprecated-calls-exclude=m.E.__add__
804-
from m import C, D, E
805-
806-
[file m.py]
807-
from typing import Union, overload
808-
from typing_extensions import deprecated
809-
810-
@deprecated("use C2 instead")
811-
class C:
812-
def __init__(self) -> None: ...
813-
814-
c: C
815-
C()
816-
C.__init__(c)
817-
818-
class D:
819-
@deprecated("use D.g instead")
820-
def f(self) -> None: ...
821-
822-
def g(self) -> None: ...
823-
824-
D.f
825-
D().f
826-
D().f()
827-
828-
class E:
829-
@overload
830-
def f(self, x: int) -> int: ...
831-
@overload
832-
def f(self, x: str) -> str: ...
833-
@deprecated("use E.f2 instead")
834-
def f(self, x: Union[int, str]) -> Union[int, str]: ...
835-
836-
@deprecated("use E.h instead")
837-
def g(self) -> None: ...
838-
839-
@overload
840-
@deprecated("no A + int")
841-
def __add__(self, v: int) -> None: ...
842-
@overload
843-
def __add__(self, v: str) -> None: ...
844-
def __add__(self, v: Union[int, str]) -> None: ...
845-
846-
E().f(1)
847-
E().f("x")
848800

849-
e = E()
850-
e.g()
851-
e + 1
852801
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)