From 4e52941a0e4c83884fe85a8a2da41994874fb591 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 2 Nov 2024 10:06:18 +0100 Subject: [PATCH 1/8] PEP 702 (@deprecated): descriptors --- mypy/checker.py | 24 ++++++++++++- mypy/checkexpr.py | 6 ++-- mypy/checkmember.py | 10 +++++- test-data/unit/check-deprecated.test | 53 ++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a650bdf2a639..90fd7daff391 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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, @@ -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, inferred_dunder_set_type, 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 @@ -7688,6 +7694,22 @@ 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: SymbolNode | None, + context: Context, + target: CallableType, + instance: Instance | None = None, + ) -> None: + """Warn if the overload item corresponding to the given callable is deprecated.""" + if isinstance(node, OverloadedFuncDef): + for item in node.items: + if isinstance(item, Decorator): + candidate = item.func.type + if instance is not None: + candidate = bind_self(candidate, instance) + if candidate == target: + self.warn_deprecated(item.func, context) + class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 577576a4e5f8..e06334746690 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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, 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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 9dc8d5475b1a..82d01d95d58b 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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: @@ -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, inferred_dunder_get_type, 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 diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index fbfdfcce5a14..dc9370e3ce39 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -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 Optional, Type, Union +from typing_extensions import deprecated, overload + +@deprecated("use E1 instead") +class D1: + def __get__(self, obj: Optional[C], objtype: Type[C]) -> Union[D1, int]: ... + +class D2: + @deprecated("use E2.__get__ instead") + def __get__(self, obj: Optional[C], objtype: Type[C]) -> 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: Type[C]) -> D3: ... + @overload + @deprecated("use E3.__get__ instead") + def __get__(self, obj: C, objtype: Type[C]) -> int: ... + def __get__(self, obj: Optional[C], objtype: Type[C]) -> 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: type[__main__.C]) -> __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: type[__main__.C]) -> 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 From 157c7c90d09b2263f4305cca9a590c9e6a7c32d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 09:20:12 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 90fd7daff391..35867cd4d7fb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7695,7 +7695,8 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: warn(deprecated, context, code=codes.DEPRECATED) def warn_deprecated_overload_item( - self, node: SymbolNode | None, + self, + node: SymbolNode | None, context: Context, target: CallableType, instance: Instance | None = None, From 281902e5f89b9878c03ab55235c4e2b70853f189 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 2 Nov 2024 10:46:21 +0100 Subject: [PATCH 3/8] fix signatures --- mypy/checker.py | 25 ++++++++++++++----------- mypy/checkexpr.py | 2 +- mypy/checkmember.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 35867cd4d7fb..9f47616212eb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4495,7 +4495,7 @@ def check_member_assignment( # Search for possible deprecations: mx.chk.check_deprecated(dunder_set, mx.context) mx.chk.warn_deprecated_overload_item( - dunder_set, mx.context, inferred_dunder_set_type, attribute_type + 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. @@ -7669,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 @@ -7682,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 @@ -7696,18 +7696,21 @@ def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: def warn_deprecated_overload_item( self, - node: SymbolNode | None, + node: Node | None, context: Context, - target: CallableType, - instance: Instance | None = None, + *, + target: Type, + selftype: Type | None = None, ) -> None: """Warn if the overload item corresponding to the given callable is deprecated.""" - if isinstance(node, OverloadedFuncDef): + target = get_proper_type(target) + if isinstance(node, OverloadedFuncDef) and isinstance(target, CallableType): for item in node.items: - if isinstance(item, Decorator): - candidate = item.func.type - if instance is not None: - candidate = bind_self(candidate, instance) + 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) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e06334746690..8d45079d8004 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1483,7 +1483,7 @@ def check_call_expr_with_callee_type( ) proper_callee = get_proper_type(callee_type) if isinstance(e.callee, NameExpr): - self.chk.warn_deprecated_overload_item(e.callee.node, e, callee_type) + 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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 82d01d95d58b..50e54ca30460 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -724,7 +724,7 @@ def analyze_descriptor_access( if not assignment: mx.chk.check_deprecated(dunder_get, mx.context) mx.chk.warn_deprecated_overload_item( - dunder_get, mx.context, inferred_dunder_get_type, descriptor_type + dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type ) inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type) From 0677dbf7b1cf77eb9b64edf0e97e21ea1f8ec4f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 09:46:53 +0000 Subject: [PATCH 4/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9f47616212eb..0e6e6eec8607 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7695,12 +7695,7 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None: warn(deprecated, context, code=codes.DEPRECATED) def warn_deprecated_overload_item( - self, - node: Node | None, - context: Context, - *, - target: Type, - selftype: Type | None = None, + 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) From a842600bb1c463a5e756f5dbcf56e7e277b5c748 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 2 Nov 2024 12:27:17 +0100 Subject: [PATCH 5/8] `--python-version 3.9` because `type` vs `Type` in output --- test-data/unit/check-deprecated.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index dc9370e3ce39..d8a74e720677 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -491,6 +491,7 @@ C().g = "x" # N: function __main__.C.g is deprecated: use g2 instead \ [case testDeprecatedDescriptor] +# flags: --python-version 3.9 from typing import Optional, Type, Union from typing_extensions import deprecated, overload From 171508570956c33304154d177d3c5e68e1b72d64 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 3 Nov 2024 09:53:12 +0100 Subject: [PATCH 6/8] Replace `Type[C]` with `Any` because `type` vs `Type` in output (`--python-version 3.9` did not help) --- test-data/unit/check-deprecated.test | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index d8a74e720677..3cedfb2d5f0d 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -491,18 +491,17 @@ C().g = "x" # N: function __main__.C.g is deprecated: use g2 instead \ [case testDeprecatedDescriptor] -# flags: --python-version 3.9 -from typing import Optional, Type, Union +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: Type[C]) -> Union[D1, int]: ... + 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: Type[C]) -> Union[D2, int]: ... + def __get__(self, obj: Optional[C], objtype: Any) -> Union[D2, int]: ... @deprecated("use E2.__set__ instead") def __set__(self, obj: C, value: int) -> None: ... @@ -510,11 +509,11 @@ class D2: class D3: @overload @deprecated("use E3.__get__ instead") - def __get__(self, obj: None, objtype: Type[C]) -> D3: ... + def __get__(self, obj: None, objtype: Any) -> D3: ... @overload @deprecated("use E3.__get__ instead") - def __get__(self, obj: C, objtype: Type[C]) -> int: ... - def __get__(self, obj: Optional[C], objtype: Type[C]) -> Union[D3, int]: ... + 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: ... @@ -537,8 +536,8 @@ 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: type[__main__.C]) -> __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: type[__main__.C]) -> builtins.int of function __main__.D3.__get__ is deprecated: use E3.__get__ 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] From 59f601cfba633b073f12dccb84b16323d28b1420 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 19 Nov 2024 23:40:21 +0100 Subject: [PATCH 7/8] Add test case `testDeprecatedImportedOverloadedFunction` and fix `check_call_expr_with_callee_type` accordingly. --- mypy/checkexpr.py | 2 +- test-data/unit/check-deprecated.test | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8d45079d8004..54fd4080ee74 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1482,7 +1482,7 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) - if isinstance(e.callee, NameExpr): + 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() diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 3cedfb2d5f0d..e6471828f9d5 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -595,3 +595,26 @@ 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] + +import m + +m.g +m.g(1) # N: 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] From d97ea2502172f97fdbaebdfe16abd93b45cf3d7f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 6 Dec 2024 23:17:13 +0100 Subject: [PATCH 8/8] adjust tests to #18192 --- test-data/unit/check-deprecated.test | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index f82d7aef0900..362d8725f183 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -504,6 +504,7 @@ C().g = "x" # E: function __main__.C.g is deprecated: use g2 instead \ [case testDeprecatedDescriptor] +# flags: --enable-error-code=deprecated from typing import Any, Optional, Union from typing_extensions import deprecated, overload @@ -536,7 +537,7 @@ class D3: def __set__(self, obj: C, value: Union[int, str]) -> None: ... class C: - d1 = D1() # N: class __main__.D1 is deprecated: use E1 instead + d1 = D1() # E: class __main__.D1 is deprecated: use E1 instead d2 = D2() d3 = D3() @@ -545,14 +546,14 @@ 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.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 # 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 # 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" # N: overload def (self: __main__.D3, obj: __main__.C, value: builtins.str) of function __main__.D3.__set__ is deprecated: use E3.__set__ instead +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] @@ -612,11 +613,12 @@ h(1.0) # E: No overload variant of "h" matches argument type "float" \ [case testDeprecatedImportedOverloadedFunction] +# flags: --enable-error-code=deprecated import m m.g -m.g(1) # N: overload def (x: builtins.int) -> builtins.int of function m.g is deprecated: work with str instead +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]