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): improve the handling of overloaded functions and methods #18682

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
18 changes: 0 additions & 18 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4648,9 +4648,6 @@ 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, 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 (
Expand Down Expand Up @@ -7894,21 +7891,6 @@ def warn_deprecated(self, node: Node | 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 and not node.is_static:
candidate = bind_self(candidate, selftype)
if candidate == target:
self.warn_deprecated(item.func, context)

# leafs

def visit_pass_stmt(self, o: PassStmt, /) -> None:
Expand Down
56 changes: 30 additions & 26 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@
validate_instance,
)
from mypy.typeops import (
bind_self,
callable_type,
custom_special_method,
erase_to_union_or_bound,
Expand Down Expand Up @@ -1476,15 +1475,6 @@ def check_call_expr_with_callee_type(
object_type=object_type,
)
proper_callee = get_proper_type(callee_type)
if isinstance(e.callee, (NameExpr, MemberExpr)):
node = e.callee.node
if node is None and member is not None and isinstance(object_type, Instance):
if (symbol := object_type.type.get(member)) is not None:
node = symbol.node
self.chk.check_deprecated(node, e)
self.chk.warn_deprecated_overload_item(
node, e, target=callee_type, selftype=object_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 Expand Up @@ -2652,6 +2642,24 @@ def check_overload_call(
context: Context,
) -> tuple[Type, Type]:
"""Checks a call to an overloaded function."""

# The following hack tries to update the `definition` attribute of the given callable
# type's items with the related decorator symbols to allow checking for deprecations:
funcdef = None
if callable_name is not None:
if isinstance(inst := get_proper_type(object_type), Instance):
if (sym := inst.type.get(callable_name.rpartition(".")[-1])) is not None:
funcdef = sym.node
else:
name_module, _, name = callable_name.rpartition(".")
if (module := self.chk.modules.get(name_module)) is not None and (
sym := module.names.get(name)
) is not None:
funcdef = sym.node
if isinstance(funcdef, OverloadedFuncDef):
for typ, defn in zip(callee.items, funcdef.items):
typ.definition = defn

# Normalize unpacked kwargs before checking the call.
callee = callee.with_unpacked_kwargs()
arg_types = self.infer_arg_types_in_empty_context(args)
Expand Down Expand Up @@ -2714,19 +2722,25 @@ def check_overload_call(
object_type,
context,
)
# If any of checks succeed, stop early.
# If any of checks succeed, perform deprecation tests and stop early.
if inferred_result is not None and unioned_result is not None:
# Both unioned and direct checks succeeded, choose the more precise type.
if (
is_subtype(inferred_result[0], unioned_result[0])
and not isinstance(get_proper_type(inferred_result[0]), AnyType)
and not none_type_var_overlap
):
return inferred_result
return unioned_result
elif unioned_result is not None:
unioned_result = None
else:
inferred_result = None
if unioned_result is not None:
for inferred_type in inferred_types:
if isinstance(c := get_proper_type(inferred_type), CallableType):
self.chk.warn_deprecated(c.definition, context)
return unioned_result
elif inferred_result is not None:
if inferred_result is not None:
if isinstance(c := get_proper_type(inferred_result[1]), CallableType):
self.chk.warn_deprecated(c.definition, context)
return inferred_result

# Step 4: Failure. At this point, we know there is no match. We fall back to trying
Expand Down Expand Up @@ -4077,21 +4091,11 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
results = []
for name, method, obj, arg in variants:
with self.msg.filter_errors(save_filtered_errors=True) as local_errors:
result = self.check_method_call(op_name, obj, method, [arg], [ARG_POS], context)
result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context)
if local_errors.has_new_errors():
errors.append(local_errors.filtered_errors())
results.append(result)
else:
if isinstance(obj, Instance) and isinstance(
defn := obj.type.get_method(name), OverloadedFuncDef
):
for item in defn.items:
if (
isinstance(item, Decorator)
and isinstance(typ := item.func.type, CallableType)
and bind_self(typ) == result[1]
):
self.chk.check_deprecated(item.func, context)
return result

# We finish invoking above operators and no early return happens. Therefore,
Expand Down
11 changes: 7 additions & 4 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING, Callable, cast

import mypy.errorcodes as codes
from mypy import message_registry, subtypes
from mypy.erasetype import erase_typevars
from mypy.expandtype import (
Expand Down Expand Up @@ -701,6 +702,10 @@ def analyze_descriptor_access(
object_type=descriptor_type,
)

deprecated_disabled = False
if assignment and codes.DEPRECATED in mx.chk.options.enabled_error_codes:
mx.chk.options.enabled_error_codes.remove(codes.DEPRECATED)
deprecated_disabled = True
_, inferred_dunder_get_type = mx.chk.expr_checker.check_call(
dunder_get_type,
[
Expand All @@ -712,12 +717,10 @@ def analyze_descriptor_access(
object_type=descriptor_type,
callable_name=callable_name,
)

if deprecated_disabled:
mx.chk.options.enabled_error_codes.add(codes.DEPRECATED)
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):
Expand Down
14 changes: 13 additions & 1 deletion mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,18 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
setter_type = snapshot_optional_type(first_item.var.setter_type)
is_trivial_body = impl.is_trivial_body if impl else False
dataclass_transform_spec = find_dataclass_transform_spec(node)

deprecated = None
if isinstance(node, FuncDef):
deprecated = node.deprecated
elif isinstance(node, OverloadedFuncDef):
deprecated_list = [node.deprecated] + [
i.func.deprecated for i in node.items if isinstance(i, Decorator)
]
deprecated_list_cleaned = [d for d in deprecated_list if d is not None]
if deprecated_list_cleaned:
deprecated = ",".join(deprecated_list_cleaned)

return (
"Func",
common,
Expand All @@ -262,7 +274,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
signature,
is_trivial_body,
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
node.deprecated if isinstance(node, FuncDef) else None,
deprecated,
setter_type, # multi-part properties are stored as OverloadedFuncDef
)
elif isinstance(node, Var):
Expand Down
74 changes: 72 additions & 2 deletions test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,11 @@ 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, overload
from typing import Any, Generic, Optional, overload, TypeVar, Union
from typing_extensions import deprecated

T = TypeVar("T")

@deprecated("use E1 instead")
class D1:
def __get__(self, obj: Optional[C], objtype: Any) -> Union[D1, int]: ...
Expand Down Expand Up @@ -701,10 +703,19 @@ class D3:
def __set__(self, obj: C, value: str) -> None: ...
def __set__(self, obj: C, value: Union[int, str]) -> None: ...

class D4(Generic[T]):
@overload
def __get__(self, obj: None, objtype: Any) -> T: ...
@overload
@deprecated("deprecated instance access")
def __get__(self, obj: C, objtype: Any) -> T: ...
def __get__(self, obj: Optional[C], objtype: Any) -> T: ...

class C:
d1 = D1() # E: class __main__.D1 is deprecated: use E1 instead
d2 = D2()
d3 = D3()
d4 = D4[int]()

c: C
C.d1
Expand All @@ -719,15 +730,21 @@ C.d3 # E: overload def (self: __main__.D3, obj: None, objtype: Any) -> __main__
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

C.d4
c.d4 # E: overload def (self: __main__.D4[T`1], obj: __main__.C, objtype: Any) -> T`1 of function __main__.D4.__get__ is deprecated: deprecated instance access
[builtins fixtures/property.pyi]


[case testDeprecatedOverloadedFunction]
# flags: --enable-error-code=deprecated

from typing import Union, overload
from typing import Any, overload, Union
from typing_extensions import deprecated

int_or_str: Union[int, str]
any: Any

@overload
def f(x: int) -> int: ...
@overload
Expand All @@ -738,6 +755,8 @@ def f(x: Union[int, str]) -> Union[int, str]: ...
f # E: function __main__.f is deprecated: use f2 instead
f(1) # E: function __main__.f is deprecated: use f2 instead
f("x") # E: function __main__.f is deprecated: use f2 instead
f(int_or_str) # E: function __main__.f is deprecated: use f2 instead
f(any) # E: function __main__.f is deprecated: use f2 instead
f(1.0) # E: function __main__.f is deprecated: use f2 instead \
# E: No overload variant of "f" matches argument type "float" \
# N: Possible overload variants: \
Expand All @@ -754,6 +773,8 @@ def g(x: Union[int, str]) -> Union[int, str]: ...
g
g(1) # E: overload def (x: builtins.int) -> builtins.int of function __main__.g is deprecated: work with str instead
g("x")
g(int_or_str) # E: overload def (x: builtins.int) -> builtins.int of function __main__.g is deprecated: work with str instead
g(any)
g(1.0) # E: No overload variant of "g" matches argument type "float" \
# N: Possible overload variants: \
# N: def g(x: int) -> int \
Expand All @@ -769,14 +790,63 @@ def h(x: Union[int, str]) -> Union[int, str]: ...
h
h(1)
h("x") # E: overload def (x: builtins.str) -> builtins.str of function __main__.h is deprecated: work with int instead
h(int_or_str) # E: overload def (x: builtins.str) -> builtins.str of function __main__.h is deprecated: work with int instead
h(any)
h(1.0) # E: No overload variant of "h" matches argument type "float" \
# N: Possible overload variants: \
# N: def h(x: int) -> int \
# N: def h(x: str) -> str

@overload
def i(x: int) -> int: ...
@overload
@deprecated("work with int instead")
def i(x: str) -> str: ...
@overload
def i(x: Any) -> Any: ...
def i(x: Union[int, str]) -> Union[int, str]: ...

i
i(1)
i("x") # E: overload def (x: builtins.str) -> builtins.str of function __main__.i is deprecated: work with int instead
i(int_or_str) # E: overload def (x: builtins.str) -> builtins.str of function __main__.i is deprecated: work with int instead
i(any)
i(1.0)

[builtins fixtures/tuple.pyi]

@overload
def j(x: int) -> int: ...
@overload
def j(x: str) -> str: ...
@overload
@deprecated("work with int or str instead")
def j(x: Any) -> Any: ...
def j(x: Union[int, str]) -> Union[int, str]: ...

j
j(1)
j("x")
j(int_or_str)
j(any) # E: overload def (x: builtins.str) -> builtins.str of function __main__.i is deprecated: work with int or str instead
j(1.0)

@overload
@deprecated("work with str instead")
def k(x: int) -> int: ...
@overload
def k(x: str) -> str: ...
@overload
@deprecated("work with str instead")
def k(x: object) -> Any: ...
def k(x: Union[int, str]) -> Union[int, str]: ...

k
k(1)
k("x")
k(int_or_str)
k(any) # E: overload def (x: builtins.str) -> builtins.str of function __main__.i is deprecated: work with int or str instead
k(1.0)
[case testDeprecatedImportedOverloadedFunction]
# flags: --enable-error-code=deprecated

Expand Down
Loading
Loading