Skip to content

Commit 8e02657

Browse files
authored
Optimize bind_self() and deprecation checks (#19556)
This contains two related optimizations: * Simplify deprecation check by removing several `bind_self()` calls and instead restoring callable type definitions in `fixup.py`, and using them during overload item matching. * Consequently, best effort filtering in `bind_self()` should be not needed anymore, since all non-trivial calls to `bind_self()` are now after `check_self_arg()` calls. There are also few micro-optimizations I noticed when looking at relevant code. In total this gives around 1% on self-check. Note: this may be not a no-op in some corner cases. If so, I will adjust overload filtering in `check_self_arg()`.
1 parent 45ee5a3 commit 8e02657

File tree

11 files changed

+114
-164
lines changed

11 files changed

+114
-164
lines changed

mypy/checker.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
726726
assert isinstance(item, Decorator)
727727
item_type = self.extract_callable_type(item.var.type, item)
728728
if item_type is not None:
729+
item_type.definition = item
729730
item_types.append(item_type)
730731
if item_types:
731732
defn.type = Overloaded(item_types)
@@ -4927,17 +4928,7 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None:
49274928
inplace, method = infer_operator_assignment_method(lvalue_type, s.op)
49284929
if inplace:
49294930
# There is __ifoo__, treat as x = x.__ifoo__(y)
4930-
rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s)
4931-
if isinstance(inst := get_proper_type(lvalue_type), Instance) and isinstance(
4932-
defn := inst.type.get_method(method), OverloadedFuncDef
4933-
):
4934-
for item in defn.items:
4935-
if (
4936-
isinstance(item, Decorator)
4937-
and isinstance(typ := item.func.type, CallableType)
4938-
and (bind_self(typ) == method_type)
4939-
):
4940-
self.warn_deprecated(item.func, s)
4931+
rvalue_type, _ = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s)
49414932
if not is_subtype(rvalue_type, lvalue_type):
49424933
self.msg.incompatible_operator_assignment(s.op, s)
49434934
else:
@@ -7962,7 +7953,7 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None:
79627953
node = node.func
79637954
if (
79647955
isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo))
7965-
and ((deprecated := node.deprecated) is not None)
7956+
and (deprecated := node.deprecated) is not None
79667957
and not self.is_typeshed_stub
79677958
and not any(
79687959
node.fullname == p or node.fullname.startswith(f"{p}.")
@@ -7972,21 +7963,6 @@ def warn_deprecated(self, node: Node | None, context: Context) -> None:
79727963
warn = self.msg.note if self.options.report_deprecated_as_note else self.msg.fail
79737964
warn(deprecated, context, code=codes.DEPRECATED)
79747965

7975-
def warn_deprecated_overload_item(
7976-
self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None
7977-
) -> None:
7978-
"""Warn if the overload item corresponding to the given callable is deprecated."""
7979-
target = get_proper_type(target)
7980-
if isinstance(node, OverloadedFuncDef) and isinstance(target, CallableType):
7981-
for item in node.items:
7982-
if isinstance(item, Decorator) and isinstance(
7983-
candidate := item.func.type, CallableType
7984-
):
7985-
if selftype is not None and not node.is_static:
7986-
candidate = bind_self(candidate, selftype)
7987-
if candidate == target:
7988-
self.warn_deprecated(item.func, context)
7989-
79907966
# leafs
79917967

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

mypy/checker_shared.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,6 @@ def check_deprecated(self, node: Node | None, context: Context) -> None:
253253
def warn_deprecated(self, node: Node | None, context: Context) -> None:
254254
raise NotImplementedError
255255

256-
@abstractmethod
257-
def warn_deprecated_overload_item(
258-
self, node: Node | None, context: Context, *, target: Type, selftype: Type | None = None
259-
) -> None:
260-
raise NotImplementedError
261-
262256
@abstractmethod
263257
def type_is_iterable(self, type: Type) -> bool:
264258
raise NotImplementedError

mypy/checkexpr.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@
129129
validate_instance,
130130
)
131131
from mypy.typeops import (
132-
bind_self,
133132
callable_type,
134133
custom_special_method,
135134
erase_to_union_or_bound,
@@ -1517,15 +1516,6 @@ def check_call_expr_with_callee_type(
15171516
object_type=object_type,
15181517
)
15191518
proper_callee = get_proper_type(callee_type)
1520-
if isinstance(e.callee, (NameExpr, MemberExpr)):
1521-
node = e.callee.node
1522-
if node is None and member is not None and isinstance(object_type, Instance):
1523-
if (symbol := object_type.type.get(member)) is not None:
1524-
node = symbol.node
1525-
self.chk.check_deprecated(node, e)
1526-
self.chk.warn_deprecated_overload_item(
1527-
node, e, target=callee_type, selftype=object_type
1528-
)
15291519
if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType):
15301520
# Cache it for find_isinstance_check()
15311521
if proper_callee.type_guard is not None:
@@ -2943,6 +2933,8 @@ def infer_overload_return_type(
29432933
# check for ambiguity due to 'Any' below.
29442934
if not args_contain_any:
29452935
self.chk.store_types(m)
2936+
if isinstance(infer_type, ProperType) and isinstance(infer_type, CallableType):
2937+
self.chk.check_deprecated(infer_type.definition, context)
29462938
return ret_type, infer_type
29472939
p_infer_type = get_proper_type(infer_type)
29482940
if isinstance(p_infer_type, CallableType):
@@ -2979,6 +2971,11 @@ def infer_overload_return_type(
29792971
else:
29802972
# Success! No ambiguity; return the first match.
29812973
self.chk.store_types(type_maps[0])
2974+
inferred_callable = inferred_types[0]
2975+
if isinstance(inferred_callable, ProperType) and isinstance(
2976+
inferred_callable, CallableType
2977+
):
2978+
self.chk.check_deprecated(inferred_callable.definition, context)
29822979
return return_types[0], inferred_types[0]
29832980

29842981
def overload_erased_call_targets(
@@ -4103,16 +4100,6 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
41034100
errors.append(local_errors.filtered_errors())
41044101
results.append(result)
41054102
else:
4106-
if isinstance(obj, Instance) and isinstance(
4107-
defn := obj.type.get_method(name), OverloadedFuncDef
4108-
):
4109-
for item in defn.items:
4110-
if (
4111-
isinstance(item, Decorator)
4112-
and isinstance(typ := item.func.type, CallableType)
4113-
and bind_self(typ) == result[1]
4114-
):
4115-
self.chk.check_deprecated(item.func, context)
41164103
return result
41174104

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

mypy/checkmember.py

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections.abc import Sequence
66
from typing import Callable, TypeVar, cast
77

8-
from mypy import message_registry, state, subtypes
8+
from mypy import message_registry, state
99
from mypy.checker_shared import TypeCheckerSharedApi
1010
from mypy.erasetype import erase_typevars
1111
from mypy.expandtype import (
@@ -14,6 +14,7 @@
1414
freshen_all_functions_type_vars,
1515
)
1616
from mypy.maptype import map_instance_to_supertype
17+
from mypy.meet import is_overlapping_types
1718
from mypy.messages import MessageBuilder
1819
from mypy.nodes import (
1920
ARG_POS,
@@ -39,6 +40,7 @@
3940
is_final_node,
4041
)
4142
from mypy.plugin import AttributeContext
43+
from mypy.subtypes import is_subtype
4244
from mypy.typeops import (
4345
bind_self,
4446
erase_to_bound,
@@ -745,10 +747,8 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
745747
callable_name=callable_name,
746748
)
747749

748-
mx.chk.check_deprecated(dunder_get, mx.context)
749-
mx.chk.warn_deprecated_overload_item(
750-
dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type
751-
)
750+
# Search for possible deprecations:
751+
mx.chk.warn_deprecated(dunder_get, mx.context)
752752

753753
inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type)
754754
if isinstance(inferred_dunder_get_type, AnyType):
@@ -825,10 +825,7 @@ def analyze_descriptor_assign(descriptor_type: Instance, mx: MemberContext) -> T
825825
)
826826

827827
# Search for possible deprecations:
828-
mx.chk.check_deprecated(dunder_set, mx.context)
829-
mx.chk.warn_deprecated_overload_item(
830-
dunder_set, mx.context, target=inferred_dunder_set_type, selftype=descriptor_type
831-
)
828+
mx.chk.warn_deprecated(dunder_set, mx.context)
832829

833830
# In the following cases, a message already will have been recorded in check_call.
834831
if (not isinstance(inferred_dunder_set_type, CallableType)) or (
@@ -1053,6 +1050,7 @@ def f(self: S) -> T: ...
10531050
new_items = []
10541051
if is_classmethod:
10551052
dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type)
1053+
p_dispatched_arg_type = get_proper_type(dispatched_arg_type)
10561054

10571055
for item in items:
10581056
if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR):
@@ -1061,28 +1059,42 @@ def f(self: S) -> T: ...
10611059
# This is pretty bad, so just return the original signature if
10621060
# there is at least one such error.
10631061
return functype
1064-
else:
1065-
selfarg = get_proper_type(item.arg_types[0])
1066-
# This matches similar special-casing in bind_self(), see more details there.
1067-
self_callable = name == "__call__" and isinstance(selfarg, CallableType)
1068-
if self_callable or subtypes.is_subtype(
1069-
dispatched_arg_type,
1070-
# This level of erasure matches the one in checker.check_func_def(),
1071-
# better keep these two checks consistent.
1072-
erase_typevars(erase_to_bound(selfarg)),
1073-
# This is to work around the fact that erased ParamSpec and TypeVarTuple
1074-
# callables are not always compatible with non-erased ones both ways.
1075-
always_covariant=any(
1076-
not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg)
1077-
),
1078-
ignore_pos_arg_names=True,
1079-
):
1080-
new_items.append(item)
1081-
elif isinstance(selfarg, ParamSpecType):
1082-
# TODO: This is not always right. What's the most reasonable thing to do here?
1083-
new_items.append(item)
1084-
elif isinstance(selfarg, TypeVarTupleType):
1085-
raise NotImplementedError
1062+
selfarg = get_proper_type(item.arg_types[0])
1063+
if isinstance(selfarg, Instance) and isinstance(p_dispatched_arg_type, Instance):
1064+
if selfarg.type is p_dispatched_arg_type.type and selfarg.args:
1065+
if not is_overlapping_types(p_dispatched_arg_type, selfarg):
1066+
# This special casing is needed since `actual <: erased(template)`
1067+
# logic below doesn't always work, and a more correct approach may
1068+
# be tricky.
1069+
continue
1070+
new_items.append(item)
1071+
1072+
if new_items:
1073+
items = new_items
1074+
new_items = []
1075+
1076+
for item in items:
1077+
selfarg = get_proper_type(item.arg_types[0])
1078+
# This matches similar special-casing in bind_self(), see more details there.
1079+
self_callable = name == "__call__" and isinstance(selfarg, CallableType)
1080+
if self_callable or is_subtype(
1081+
dispatched_arg_type,
1082+
# This level of erasure matches the one in checker.check_func_def(),
1083+
# better keep these two checks consistent.
1084+
erase_typevars(erase_to_bound(selfarg)),
1085+
# This is to work around the fact that erased ParamSpec and TypeVarTuple
1086+
# callables are not always compatible with non-erased ones both ways.
1087+
always_covariant=any(
1088+
not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg)
1089+
),
1090+
ignore_pos_arg_names=True,
1091+
):
1092+
new_items.append(item)
1093+
elif isinstance(selfarg, ParamSpecType):
1094+
# TODO: This is not always right. What's the most reasonable thing to do here?
1095+
new_items.append(item)
1096+
elif isinstance(selfarg, TypeVarTupleType):
1097+
raise NotImplementedError
10861098
if not new_items:
10871099
# Choose first item for the message (it may be not very helpful for overloads).
10881100
msg.incompatible_self_argument(

mypy/erasetype.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,29 +145,34 @@ def erase_typevars(t: Type, ids_to_erase: Container[TypeVarId] | None = None) ->
145145
or just the ones in the provided collection.
146146
"""
147147

148+
if ids_to_erase is None:
149+
return t.accept(TypeVarEraser(None, AnyType(TypeOfAny.special_form)))
150+
148151
def erase_id(id: TypeVarId) -> bool:
149-
if ids_to_erase is None:
150-
return True
151152
return id in ids_to_erase
152153

153154
return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form)))
154155

155156

157+
def erase_meta_id(id: TypeVarId) -> bool:
158+
return id.is_meta_var()
159+
160+
156161
def replace_meta_vars(t: Type, target_type: Type) -> Type:
157162
"""Replace unification variables in a type with the target type."""
158-
return t.accept(TypeVarEraser(lambda id: id.is_meta_var(), target_type))
163+
return t.accept(TypeVarEraser(erase_meta_id, target_type))
159164

160165

161166
class TypeVarEraser(TypeTranslator):
162167
"""Implementation of type erasure"""
163168

164-
def __init__(self, erase_id: Callable[[TypeVarId], bool], replacement: Type) -> None:
169+
def __init__(self, erase_id: Callable[[TypeVarId], bool] | None, replacement: Type) -> None:
165170
super().__init__()
166171
self.erase_id = erase_id
167172
self.replacement = replacement
168173

169174
def visit_type_var(self, t: TypeVarType) -> Type:
170-
if self.erase_id(t.id):
175+
if self.erase_id is None or self.erase_id(t.id):
171176
return self.replacement
172177
return t
173178

@@ -212,12 +217,12 @@ def visit_callable_type(self, t: CallableType) -> Type:
212217
return result
213218

214219
def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
215-
if self.erase_id(t.id):
220+
if self.erase_id is None or self.erase_id(t.id):
216221
return t.tuple_fallback.copy_modified(args=[self.replacement])
217222
return t
218223

219224
def visit_param_spec(self, t: ParamSpecType) -> Type:
220-
if self.erase_id(t.id):
225+
if self.erase_id is None or self.erase_id(t.id):
221226
return self.replacement
222227
return t
223228

mypy/fixup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ def visit_func_def(self, func: FuncDef) -> None:
165165
func.info = self.current_info
166166
if func.type is not None:
167167
func.type.accept(self.type_fixer)
168+
if isinstance(func.type, CallableType):
169+
func.type.definition = func
168170

169171
def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
170172
if self.current_info is not None:

mypy/meet.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,18 @@ def is_object(t: ProperType) -> bool:
294294
return isinstance(t, Instance) and t.type.fullname == "builtins.object"
295295

296296

297+
def is_none_typevarlike_overlap(t1: ProperType, t2: ProperType) -> bool:
298+
return isinstance(t1, NoneType) and isinstance(t2, TypeVarLikeType)
299+
300+
301+
def is_none_object_overlap(t1: ProperType, t2: ProperType) -> bool:
302+
return (
303+
isinstance(t1, NoneType)
304+
and isinstance(t2, Instance)
305+
and t2.type.fullname == "builtins.object"
306+
)
307+
308+
297309
def is_overlapping_types(
298310
left: Type,
299311
right: Type,
@@ -383,14 +395,6 @@ def _is_overlapping_types(left: Type, right: Type) -> bool:
383395
):
384396
return True
385397

386-
def is_none_object_overlap(t1: Type, t2: Type) -> bool:
387-
t1, t2 = get_proper_types((t1, t2))
388-
return (
389-
isinstance(t1, NoneType)
390-
and isinstance(t2, Instance)
391-
and t2.type.fullname == "builtins.object"
392-
)
393-
394398
if overlap_for_overloads:
395399
if is_none_object_overlap(left, right) or is_none_object_overlap(right, left):
396400
return False
@@ -420,10 +424,6 @@ def _is_subtype(left: Type, right: Type) -> bool:
420424
# If both types are singleton variants (and are not TypeVarLikes), we've hit the base case:
421425
# we skip these checks to avoid infinitely recursing.
422426

423-
def is_none_typevarlike_overlap(t1: Type, t2: Type) -> bool:
424-
t1, t2 = get_proper_types((t1, t2))
425-
return isinstance(t1, NoneType) and isinstance(t2, TypeVarLikeType)
426-
427427
if prohibit_none_typevar_overlap:
428428
if is_none_typevarlike_overlap(left, right) or is_none_typevarlike_overlap(right, left):
429429
return False

mypy/nodes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,12 +595,14 @@ def is_trivial_self(self) -> bool:
595595
"""
596596
if self._is_trivial_self is not None:
597597
return self._is_trivial_self
598-
for item in self.items:
598+
for i, item in enumerate(self.items):
599+
# Note: bare @property is removed in visit_decorator().
600+
trivial = 1 if i > 0 or not self.is_property else 0
599601
if isinstance(item, FuncDef):
600602
if not item.is_trivial_self:
601603
self._is_trivial_self = False
602604
return False
603-
elif item.decorators or not item.func.is_trivial_self:
605+
elif len(item.decorators) > trivial or not item.func.is_trivial_self:
604606
self._is_trivial_self = False
605607
return False
606608
self._is_trivial_self = True

0 commit comments

Comments
 (0)