From f229456ae09ed8bbbf53022d68a76ce0530201c3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 4 May 2025 13:54:53 +0100 Subject: [PATCH 1/4] Speed up bind_self() in trivial cases --- mypy/checkmember.py | 114 +++++++++++++++++++++++++++++++++++++------- mypy/nodes.py | 30 +++++++++++- mypy/semanal.py | 1 + mypy/typeops.py | 8 ++++ 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1a76372d4731..4bfe63363142 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Callable, cast +from typing import Callable, TypeVar, cast from mypy import message_registry, state, subtypes from mypy.checker_shared import TypeCheckerSharedApi @@ -18,6 +18,7 @@ from mypy.nodes import ( ARG_POS, ARG_STAR, + ARG_STAR2, EXCLUDED_ENUM_ATTRIBUTES, SYMBOL_FUNCBASE_TYPES, Context, @@ -357,10 +358,15 @@ def analyze_instance_member_access( signature = method.type signature = freshen_all_functions_type_vars(signature) if not method.is_static: - signature = check_self_arg( - signature, mx.self_type, method.is_class, mx.context, name, mx.msg - ) - signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class) + if isinstance(method, (FuncDef, OverloadedFuncDef)) and method.is_trivial_self: + signature = bind_self_fast(signature, mx.self_type) + else: + signature = check_self_arg( + signature, mx.self_type, method.is_class, mx.context, name, mx.msg + ) + signature = bind_self( + signature, mx.self_type, is_classmethod=method.is_class, no_overlap_check=True + ) # TODO: should we skip these steps for static methods as well? # Since generic static methods should not be allowed. typ = map_instance_to_supertype(typ, method.info) @@ -519,9 +525,11 @@ def analyze_member_var_access( mx.chk.warn_deprecated(v, mx.context) vv = v + is_trivial_self = False if isinstance(vv, Decorator): # The associated Var node of a decorator contains the type. v = vv.var + is_trivial_self = vv.func.is_trivial_self and not vv.decorators if mx.is_super and not mx.suppress_errors: validate_super_call(vv.func, mx) @@ -553,7 +561,7 @@ def analyze_member_var_access( if mx.is_lvalue and not mx.chk.get_final_context(): check_final_member(name, info, mx.msg, mx.context) - return analyze_var(name, v, itype, mx, implicit=implicit) + return analyze_var(name, v, itype, mx, implicit=implicit, is_trivial_self=is_trivial_self) elif isinstance(v, FuncDef): assert False, "Did not expect a function" elif isinstance(v, MypyFile): @@ -848,7 +856,13 @@ def is_instance_var(var: Var) -> bool: def analyze_var( - name: str, var: Var, itype: Instance, mx: MemberContext, *, implicit: bool = False + name: str, + var: Var, + itype: Instance, + mx: MemberContext, + *, + implicit: bool = False, + is_trivial_self: bool = False, ) -> Type: """Analyze access to an attribute via a Var node. @@ -856,6 +870,7 @@ def analyze_var( itype is the instance type in which attribute should be looked up original_type is the type of E in the expression E.var if implicit is True, the original Var was created as an assignment to self + if is_trivial_self is True, we can use fast path for bind_self(). """ # Found a member variable. original_itype = itype @@ -902,7 +917,7 @@ def analyze_var( for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]: p_ct = get_proper_type(ct) if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj(): - item = expand_and_bind_callable(p_ct, var, itype, name, mx) + item = expand_and_bind_callable(p_ct, var, itype, name, mx, is_trivial_self) else: item = expand_without_binding(ct, var, itype, original_itype, mx) bound_items.append(item) @@ -936,13 +951,21 @@ def expand_without_binding( def expand_and_bind_callable( - functype: FunctionLike, var: Var, itype: Instance, name: str, mx: MemberContext + functype: FunctionLike, + var: Var, + itype: Instance, + name: str, + mx: MemberContext, + is_trivial_self: bool, ) -> Type: functype = freshen_all_functions_type_vars(functype) typ = get_proper_type(expand_self_type(var, functype, mx.original_type)) assert isinstance(typ, FunctionLike) - typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg) - typ = bind_self(typ, mx.self_type, var.is_classmethod) + if is_trivial_self: + typ = bind_self_fast(typ, mx.self_type) + else: + typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg) + typ = bind_self(typ, mx.self_type, var.is_classmethod, no_overlap_check=True) expanded = expand_type_by_instance(typ, itype) freeze_all_type_vars(expanded) if not var.is_property: @@ -1201,10 +1224,22 @@ def analyze_class_attribute_access( isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_static ) t = get_proper_type(t) - if isinstance(t, FunctionLike) and is_classmethod: + is_trivial_self = False + if isinstance(node.node, Decorator): + # Use fast path if there are trivial decorators like @classmethod or @property + is_trivial_self = node.node.func.is_trivial_self and not node.node.decorators + elif isinstance(node.node, (FuncDef, OverloadedFuncDef)): + is_trivial_self = node.node.is_trivial_self + if isinstance(t, FunctionLike) and is_classmethod and not is_trivial_self: t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg) result = add_class_tvars( - t, isuper, is_classmethod, is_staticmethod, mx.self_type, original_vars=original_vars + t, + isuper, + is_classmethod, + is_staticmethod, + mx.self_type, + original_vars=original_vars, + is_trivial_self=is_trivial_self, ) # __set__ is not called on class objects. if not mx.is_lvalue: @@ -1253,7 +1288,7 @@ def analyze_class_attribute_access( # Annotated and/or explicit class methods go through other code paths above, for # unannotated implicit class methods we do this here. if node.node.is_class: - typ = bind_self(typ, is_classmethod=True) + typ = bind_self_fast(typ) return apply_class_attr_hook(mx, hook, typ) @@ -1340,6 +1375,7 @@ def add_class_tvars( is_staticmethod: bool, original_type: Type, original_vars: Sequence[TypeVarLikeType] | None = None, + is_trivial_self: bool = False, ) -> Type: """Instantiate type variables during analyze_class_attribute_access, e.g T and Q in the following: @@ -1360,6 +1396,7 @@ class B(A[str]): pass original_type: The value of the type B in the expression B.foo() or the corresponding component in case of a union (this is used to bind the self-types) original_vars: Type variables of the class callable on which the method was accessed + is_trivial_self: if True, we can use fast path for bind_self(). Returns: Expanded method type with added type variables (when needed). """ @@ -1381,7 +1418,10 @@ class B(A[str]): pass tvars = original_vars if original_vars is not None else [] t = freshen_all_functions_type_vars(t) if is_classmethod: - t = bind_self(t, original_type, is_classmethod=True) + if is_trivial_self: + t = bind_self_fast(t, original_type) + else: + t = bind_self(t, original_type, is_classmethod=True, no_overlap_check=True) if is_classmethod or is_staticmethod: assert isuper is not None t = expand_type_by_instance(t, isuper) @@ -1420,5 +1460,47 @@ def analyze_decorator_or_funcbase_access( if isinstance(defn, Decorator): return analyze_var(name, defn.var, itype, mx) typ = function_type(defn, mx.chk.named_type("builtins.function")) + is_trivial_self = False + if isinstance(defn, Decorator): + # Use fast path if there are trivial decorators like @classmethod or @property + is_trivial_self = defn.func.is_trivial_self and not defn.decorators + elif isinstance(defn, (FuncDef, OverloadedFuncDef)): + is_trivial_self = defn.is_trivial_self + if is_trivial_self: + return bind_self_fast(typ, mx.self_type) typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg) - return bind_self(typ, original_type=mx.self_type, is_classmethod=defn.is_class) + return bind_self( + typ, original_type=mx.self_type, is_classmethod=defn.is_class, no_overlap_check=True + ) + + +F = TypeVar("F", bound=FunctionLike) + + +def bind_self_fast(method: F, original_type: Type | None = None) -> F: + """Return a copy of `method`, with the type of its first parameter (usually + self or cls) bound to original_type. + + This is a faster version of mypy.typeops.bind_self() that can be used for methods + with trivial self/cls annotations. + """ + if isinstance(method, Overloaded): + items = [bind_self_fast(c, original_type) for c in method.items] + return cast(F, Overloaded(items)) + assert isinstance(method, CallableType) + if not method.arg_types: + # Invalid method, return something. + return cast(F, method) + if method.arg_kinds[0] in (ARG_STAR, ARG_STAR2): + # See typeops.py for details. + return cast(F, method) + original_type = get_proper_type(original_type) + if isinstance(original_type, CallableType) and original_type.is_type_obj(): + original_type = TypeType.make_normalized(original_type.ret_type) + res = method.copy_modified( + arg_types=method.arg_types[1:], + arg_kinds=method.arg_kinds[1:], + arg_names=method.arg_names[1:], + bound_args=[original_type], + ) + return cast(F, res) diff --git a/mypy/nodes.py b/mypy/nodes.py index 45c59e0c765e..584e56667944 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -550,7 +550,7 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement): Overloaded variants must be consecutive in the source file. """ - __slots__ = ("items", "unanalyzed_items", "impl", "deprecated") + __slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self") items: list[OverloadPart] unanalyzed_items: list[OverloadPart] @@ -563,6 +563,7 @@ def __init__(self, items: list[OverloadPart]) -> None: self.unanalyzed_items = items.copy() self.impl = None self.deprecated = None + self._is_trivial_self: bool | None = None if items: # TODO: figure out how to reliably set end position (we don't know the impl here). self.set_line(items[0].line, items[0].column) @@ -576,6 +577,27 @@ def name(self) -> str: assert self.impl is not None return self.impl.name + @property + def is_trivial_self(self) -> bool: + """Check we can use bind_self() fast path for this overload. + + This will return False if at least one overload: + * Has an explicit self annotation, or Self in signature. + * Has a non-trivial decorator. + """ + if self._is_trivial_self is not None: + return self._is_trivial_self + for item in self.items: + if isinstance(item, FuncDef): + if not item.is_trivial_self: + self._is_trivial_self = False + return False + elif item.decorators or not item.func.is_trivial_self: + self._is_trivial_self = False + return False + self._is_trivial_self = True + return True + def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_overloaded_func_def(self) @@ -747,6 +769,7 @@ def is_dynamic(self) -> bool: "is_decorated", "is_conditional", "is_trivial_body", + "is_trivial_self", "is_mypy_only", ] @@ -771,6 +794,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): "abstract_status", "original_def", "is_trivial_body", + "is_trivial_self", "is_mypy_only", # Present only when a function is decorated with @typing.dataclass_transform or similar "dataclass_transform_spec", @@ -804,6 +828,10 @@ def __init__( self.dataclass_transform_spec: DataclassTransformSpec | None = None self.docstring: str | None = None self.deprecated: str | None = None + # This is used to simplify bind_self() logic in trivial cases (which are + # the majority). In cases where self is not annotated and there are no Self + # in the signature we can simply drop the first argument. + self.is_trivial_self = False @property def name(self) -> str: diff --git a/mypy/semanal.py b/mypy/semanal.py index 1b592e722cb4..89bb5ab97c2a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1085,6 +1085,7 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: assert self.type is not None and self.type.self_type is not None leading_type: Type = self.type.self_type else: + func.is_trivial_self = True leading_type = fill_typevars(info) if func.is_class or func.name == "__new__": leading_type = self.class_type(leading_type) diff --git a/mypy/typeops.py b/mypy/typeops.py index bcf946900563..26e55720106e 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -368,6 +368,7 @@ def bind_self( original_type: Type | None = None, is_classmethod: bool = False, ignore_instances: bool = False, + no_overlap_check: bool = False, ) -> F: """Return a copy of `method`, with the type of its first parameter (usually self or cls) bound to original_type. @@ -390,8 +391,15 @@ class B(A): pass b = B().copy() # type: B + If no_overlap_check is True, the caller already checked that original_type + is a valid self type and filtered relevant overload items. """ if isinstance(method, Overloaded): + if no_overlap_check: + items = [ + bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items + ] + return cast(F, Overloaded(items)) items = [] original_type = get_proper_type(original_type) for c in method.items: From 517bce7cb625c9554e927584aba2d2c17c6f99be Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 4 May 2025 17:19:02 +0100 Subject: [PATCH 2/4] Try fixing edge case in check_self_arg() --- mypy/checker.py | 8 ++++++-- mypy/checkmember.py | 9 ++------- mypy/erasetype.py | 21 ++++++++++++++++++--- mypy/nodes.py | 2 +- mypy/typeshed/stdlib/os/__init__.pyi | 2 +- mypy/typeshed/stdlib/weakref.pyi | 2 +- test-data/unit/check-overloading.test | 2 +- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2d82d74cc197..ce773a90045e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1373,8 +1373,12 @@ def check_func_def( if not is_same_type(arg_type, ref_type): # This level of erasure matches the one in checkmember.check_self_arg(), # better keep these two checks consistent. - erased = get_proper_type(erase_typevars(erase_to_bound(arg_type))) - if not is_subtype(ref_type, erased, ignore_type_params=True): + erased = get_proper_type( + erase_typevars(erase_to_bound(arg_type), use_upper_bound=True) + ) + if not is_subtype( + ref_type, erased, ignore_type_params=True, always_covariant=True + ): if ( isinstance(erased, Instance) and erased.type.is_protocol diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4bfe63363142..d8a8c8da661f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -44,7 +44,6 @@ erase_to_bound, freeze_all_type_vars, function_type, - get_all_type_vars, get_type_vars, make_simplified_union, supported_self_type, @@ -1060,12 +1059,8 @@ def f(self: S) -> T: ... # better keep these two checks consistent. if subtypes.is_subtype( dispatched_arg_type, - erase_typevars(erase_to_bound(selfarg)), - # This is to work around the fact that erased ParamSpec and TypeVarTuple - # callables are not always compatible with non-erased ones both ways. - always_covariant=any( - not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg) - ), + erase_typevars(erase_to_bound(selfarg), use_upper_bound=True), + always_covariant=True, ignore_pos_arg_names=True, ): new_items.append(item) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 6c47670d6687..331a22f1e689 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -140,7 +140,9 @@ def visit_type_alias_type(self, t: TypeAliasType) -> ProperType: raise RuntimeError("Type aliases should be expanded before accepting this visitor") -def erase_typevars(t: Type, ids_to_erase: Container[TypeVarId] | None = None) -> Type: +def erase_typevars( + t: Type, ids_to_erase: Container[TypeVarId] | None = None, use_upper_bound: bool = False +) -> Type: """Replace all type variables in a type with any, or just the ones in the provided collection. """ @@ -150,7 +152,7 @@ def erase_id(id: TypeVarId) -> bool: return True return id in ids_to_erase - return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form))) + return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form), use_upper_bound)) def replace_meta_vars(t: Type, target_type: Type) -> Type: @@ -161,13 +163,21 @@ def replace_meta_vars(t: Type, target_type: Type) -> Type: class TypeVarEraser(TypeTranslator): """Implementation of type erasure""" - def __init__(self, erase_id: Callable[[TypeVarId], bool], replacement: Type) -> None: + def __init__( + self, + erase_id: Callable[[TypeVarId], bool], + replacement: Type, + use_upper_bound: bool = False, + ) -> None: super().__init__() self.erase_id = erase_id self.replacement = replacement + self.use_upper_bound = use_upper_bound def visit_type_var(self, t: TypeVarType) -> Type: if self.erase_id(t.id): + if self.use_upper_bound: + return t.upper_bound return self.replacement return t @@ -204,11 +214,16 @@ def visit_tuple_type(self, t: TupleType) -> Type: return result def visit_callable_type(self, t: CallableType) -> Type: + use_upper_bound = self.use_upper_bound + # This is to work around the fact that erased callables are not compatible + # with non-erased ones (due to contravariance in arg types). + self.use_upper_bound = False result = super().visit_callable_type(t) assert isinstance(result, ProperType) and isinstance(result, CallableType) # Usually this is done in semanal_typeargs.py, but erasure can create # a non-normal callable from normal one. result.normalize_trivial_unpack() + self.use_upper_bound = use_upper_bound return result def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: diff --git a/mypy/nodes.py b/mypy/nodes.py index 584e56667944..a9cf9f2b32a4 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -592,7 +592,7 @@ def is_trivial_self(self) -> bool: if not item.is_trivial_self: self._is_trivial_self = False return False - elif item.decorators or not item.func.is_trivial_self: + elif len(item.decorators) > 1 or not item.func.is_trivial_self: self._is_trivial_self = False return False self._is_trivial_self = True diff --git a/mypy/typeshed/stdlib/os/__init__.pyi b/mypy/typeshed/stdlib/os/__init__.pyi index 4a7c03632a67..ee9f0977bd26 100644 --- a/mypy/typeshed/stdlib/os/__init__.pyi +++ b/mypy/typeshed/stdlib/os/__init__.pyi @@ -721,7 +721,7 @@ class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): unsetenv: Callable[[AnyStr, AnyStr], object], ) -> None: ... - def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... + def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... # type: ignore[override] def copy(self) -> dict[AnyStr, AnyStr]: ... def __delitem__(self, key: AnyStr) -> None: ... def __getitem__(self, key: AnyStr) -> AnyStr: ... diff --git a/mypy/typeshed/stdlib/weakref.pyi b/mypy/typeshed/stdlib/weakref.pyi index 05a7b2bcda66..ff974d450e51 100644 --- a/mypy/typeshed/stdlib/weakref.pyi +++ b/mypy/typeshed/stdlib/weakref.pyi @@ -110,7 +110,7 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): def items(self) -> Iterator[tuple[_KT, _VT]]: ... # type: ignore[override] def itervaluerefs(self) -> Iterator[KeyedRef[_KT, _VT]]: ... def valuerefs(self) -> list[KeyedRef[_KT, _VT]]: ... - def setdefault(self, key: _KT, default: _VT) -> _VT: ... + def setdefault(self, key: _KT, default: _VT) -> _VT: ... # type: ignore[override] @overload def pop(self, key: _KT) -> _VT: ... @overload diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 243568c54253..6452a82d7c6b 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6762,7 +6762,7 @@ class D(Generic[T]): def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... - def f(Self, x): ... + def f(self, x): ... a: D[str] # E: Type argument "str" of "D" must be a subtype of "C" reveal_type(a.f(1)) # N: Revealed type is "builtins.int" From 4d216e00bba8b56ce9216bce5bbfb309fe3a4de3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 4 May 2025 19:09:28 +0100 Subject: [PATCH 3/4] Revert "Try fixing edge case in check_self_arg()" This reverts commit 517bce7cb625c9554e927584aba2d2c17c6f99be. --- mypy/checker.py | 8 ++------ mypy/checkmember.py | 9 +++++++-- mypy/erasetype.py | 21 +++------------------ mypy/nodes.py | 2 +- mypy/typeshed/stdlib/os/__init__.pyi | 2 +- mypy/typeshed/stdlib/weakref.pyi | 2 +- test-data/unit/check-overloading.test | 2 +- 7 files changed, 16 insertions(+), 30 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ce773a90045e..2d82d74cc197 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1373,12 +1373,8 @@ def check_func_def( if not is_same_type(arg_type, ref_type): # This level of erasure matches the one in checkmember.check_self_arg(), # better keep these two checks consistent. - erased = get_proper_type( - erase_typevars(erase_to_bound(arg_type), use_upper_bound=True) - ) - if not is_subtype( - ref_type, erased, ignore_type_params=True, always_covariant=True - ): + erased = get_proper_type(erase_typevars(erase_to_bound(arg_type))) + if not is_subtype(ref_type, erased, ignore_type_params=True): if ( isinstance(erased, Instance) and erased.type.is_protocol diff --git a/mypy/checkmember.py b/mypy/checkmember.py index d8a8c8da661f..4bfe63363142 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -44,6 +44,7 @@ erase_to_bound, freeze_all_type_vars, function_type, + get_all_type_vars, get_type_vars, make_simplified_union, supported_self_type, @@ -1059,8 +1060,12 @@ def f(self: S) -> T: ... # better keep these two checks consistent. if subtypes.is_subtype( dispatched_arg_type, - erase_typevars(erase_to_bound(selfarg), use_upper_bound=True), - always_covariant=True, + erase_typevars(erase_to_bound(selfarg)), + # This is to work around the fact that erased ParamSpec and TypeVarTuple + # callables are not always compatible with non-erased ones both ways. + always_covariant=any( + not isinstance(tv, TypeVarType) for tv in get_all_type_vars(selfarg) + ), ignore_pos_arg_names=True, ): new_items.append(item) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 331a22f1e689..6c47670d6687 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -140,9 +140,7 @@ def visit_type_alias_type(self, t: TypeAliasType) -> ProperType: raise RuntimeError("Type aliases should be expanded before accepting this visitor") -def erase_typevars( - t: Type, ids_to_erase: Container[TypeVarId] | None = None, use_upper_bound: bool = False -) -> Type: +def erase_typevars(t: Type, ids_to_erase: Container[TypeVarId] | None = None) -> Type: """Replace all type variables in a type with any, or just the ones in the provided collection. """ @@ -152,7 +150,7 @@ def erase_id(id: TypeVarId) -> bool: return True return id in ids_to_erase - return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form), use_upper_bound)) + return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form))) def replace_meta_vars(t: Type, target_type: Type) -> Type: @@ -163,21 +161,13 @@ def replace_meta_vars(t: Type, target_type: Type) -> Type: class TypeVarEraser(TypeTranslator): """Implementation of type erasure""" - def __init__( - self, - erase_id: Callable[[TypeVarId], bool], - replacement: Type, - use_upper_bound: bool = False, - ) -> None: + def __init__(self, erase_id: Callable[[TypeVarId], bool], replacement: Type) -> None: super().__init__() self.erase_id = erase_id self.replacement = replacement - self.use_upper_bound = use_upper_bound def visit_type_var(self, t: TypeVarType) -> Type: if self.erase_id(t.id): - if self.use_upper_bound: - return t.upper_bound return self.replacement return t @@ -214,16 +204,11 @@ def visit_tuple_type(self, t: TupleType) -> Type: return result def visit_callable_type(self, t: CallableType) -> Type: - use_upper_bound = self.use_upper_bound - # This is to work around the fact that erased callables are not compatible - # with non-erased ones (due to contravariance in arg types). - self.use_upper_bound = False result = super().visit_callable_type(t) assert isinstance(result, ProperType) and isinstance(result, CallableType) # Usually this is done in semanal_typeargs.py, but erasure can create # a non-normal callable from normal one. result.normalize_trivial_unpack() - self.use_upper_bound = use_upper_bound return result def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: diff --git a/mypy/nodes.py b/mypy/nodes.py index a9cf9f2b32a4..584e56667944 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -592,7 +592,7 @@ def is_trivial_self(self) -> bool: if not item.is_trivial_self: self._is_trivial_self = False return False - elif len(item.decorators) > 1 or not item.func.is_trivial_self: + elif item.decorators or not item.func.is_trivial_self: self._is_trivial_self = False return False self._is_trivial_self = True diff --git a/mypy/typeshed/stdlib/os/__init__.pyi b/mypy/typeshed/stdlib/os/__init__.pyi index ee9f0977bd26..4a7c03632a67 100644 --- a/mypy/typeshed/stdlib/os/__init__.pyi +++ b/mypy/typeshed/stdlib/os/__init__.pyi @@ -721,7 +721,7 @@ class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): unsetenv: Callable[[AnyStr, AnyStr], object], ) -> None: ... - def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... # type: ignore[override] + def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... def copy(self) -> dict[AnyStr, AnyStr]: ... def __delitem__(self, key: AnyStr) -> None: ... def __getitem__(self, key: AnyStr) -> AnyStr: ... diff --git a/mypy/typeshed/stdlib/weakref.pyi b/mypy/typeshed/stdlib/weakref.pyi index ff974d450e51..05a7b2bcda66 100644 --- a/mypy/typeshed/stdlib/weakref.pyi +++ b/mypy/typeshed/stdlib/weakref.pyi @@ -110,7 +110,7 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): def items(self) -> Iterator[tuple[_KT, _VT]]: ... # type: ignore[override] def itervaluerefs(self) -> Iterator[KeyedRef[_KT, _VT]]: ... def valuerefs(self) -> list[KeyedRef[_KT, _VT]]: ... - def setdefault(self, key: _KT, default: _VT) -> _VT: ... # type: ignore[override] + def setdefault(self, key: _KT, default: _VT) -> _VT: ... @overload def pop(self, key: _KT) -> _VT: ... @overload diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 6452a82d7c6b..243568c54253 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6762,7 +6762,7 @@ class D(Generic[T]): def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... - def f(self, x): ... + def f(Self, x): ... a: D[str] # E: Type argument "str" of "D" must be a subtype of "C" reveal_type(a.f(1)) # N: Revealed type is "builtins.int" From 2e4b710664e9669614589e4ab57c37e3e8609008 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 4 May 2025 19:16:25 +0100 Subject: [PATCH 4/4] Undo the no_overlap_check optimization --- mypy/checkmember.py | 12 ++++-------- mypy/typeops.py | 8 -------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4bfe63363142..85f6bd6de3c0 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -364,9 +364,7 @@ def analyze_instance_member_access( signature = check_self_arg( signature, mx.self_type, method.is_class, mx.context, name, mx.msg ) - signature = bind_self( - signature, mx.self_type, is_classmethod=method.is_class, no_overlap_check=True - ) + signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class) # TODO: should we skip these steps for static methods as well? # Since generic static methods should not be allowed. typ = map_instance_to_supertype(typ, method.info) @@ -965,7 +963,7 @@ def expand_and_bind_callable( typ = bind_self_fast(typ, mx.self_type) else: typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg) - typ = bind_self(typ, mx.self_type, var.is_classmethod, no_overlap_check=True) + typ = bind_self(typ, mx.self_type, var.is_classmethod) expanded = expand_type_by_instance(typ, itype) freeze_all_type_vars(expanded) if not var.is_property: @@ -1421,7 +1419,7 @@ class B(A[str]): pass if is_trivial_self: t = bind_self_fast(t, original_type) else: - t = bind_self(t, original_type, is_classmethod=True, no_overlap_check=True) + t = bind_self(t, original_type, is_classmethod=True) if is_classmethod or is_staticmethod: assert isuper is not None t = expand_type_by_instance(t, isuper) @@ -1469,9 +1467,7 @@ def analyze_decorator_or_funcbase_access( if is_trivial_self: return bind_self_fast(typ, mx.self_type) typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg) - return bind_self( - typ, original_type=mx.self_type, is_classmethod=defn.is_class, no_overlap_check=True - ) + return bind_self(typ, original_type=mx.self_type, is_classmethod=defn.is_class) F = TypeVar("F", bound=FunctionLike) diff --git a/mypy/typeops.py b/mypy/typeops.py index 26e55720106e..bcf946900563 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -368,7 +368,6 @@ def bind_self( original_type: Type | None = None, is_classmethod: bool = False, ignore_instances: bool = False, - no_overlap_check: bool = False, ) -> F: """Return a copy of `method`, with the type of its first parameter (usually self or cls) bound to original_type. @@ -391,15 +390,8 @@ class B(A): pass b = B().copy() # type: B - If no_overlap_check is True, the caller already checked that original_type - is a valid self type and filtered relevant overload items. """ if isinstance(method, Overloaded): - if no_overlap_check: - items = [ - bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items - ] - return cast(F, Overloaded(items)) items = [] original_type = get_proper_type(original_type) for c in method.items: