From 9b079f6592740a51c0e629728eeb0324ad85126f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 Jun 2025 13:25:18 +0100 Subject: [PATCH 1/9] Bump version to 1.16.1+dev --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index 34fb2f642c5e..d2d42a386dec 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.16.0" +__version__ = "1.16.1+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From 0a4f28431faa18e59d35bc269cb0ea6c00810653 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 3 Jun 2025 08:18:05 +0100 Subject: [PATCH 2/9] Fix crash on invalid property inside its own body (#19208) Fixes https://github.com/python/mypy/issues/19205 --- mypy/checkmember.py | 4 ++++ test-data/unit/check-classes.test | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index cc104fed0752..4821c622cb0c 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -974,6 +974,10 @@ def expand_and_bind_callable( assert isinstance(expanded, CallableType) if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: # TODO: use check_call() to infer better type, same as for __set__(). + if not expanded.arg_types: + # This can happen when accessing invalid property from its own body, + # error will be reported elsewhere. + return AnyType(TypeOfAny.from_error) return expanded.arg_types[0] else: return expanded.ret_type diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e0ea00aee361..59204cc31c43 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8726,3 +8726,16 @@ class Fields: reveal_type(Fields.bool_f) # N: Revealed type is "__main__.BoolField" reveal_type(Fields.int_f) # N: Revealed type is "__main__.NumField" reveal_type(Fields.custom_f) # N: Revealed type is "__main__.AnyField[__main__.Custom]" + +[case testRecursivePropertyWithInvalidSetterNoCrash] +class NoopPowerResource: + _hardware_type: int + + @property + def hardware_type(self) -> int: + return self._hardware_type + + @hardware_type.setter + def hardware_type(self) -> None: # E: Invalid property setter signature + self.hardware_type = None # Note: intentionally recursive +[builtins fixtures/property.pyi] From c39f5e73c47182e51c5d8d488f7cc7301257c974 Mon Sep 17 00:00:00 2001 From: Advait Dixit <48302999+advait-dixit@users.noreply.github.com> Date: Mon, 2 Jun 2025 07:20:25 -0700 Subject: [PATCH 3/9] [mypyc] Fixing condition for handling user-defined __del__ (#19188) Fixes #19175. Conditions for generating and invoking `del` method were not consistent. As things currently stand, this pull request fixes the crash. However, for classes that derive from Python built-ins, user-defined `__del__` will not be invoked. --- mypyc/codegen/emitclass.py | 6 +++--- mypyc/test-data/run-classes.test | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index c5191e5fb939..9cb9074b9fc4 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -304,9 +304,6 @@ def emit_line() -> None: emit_line() generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter) emit_line() - if del_method: - generate_finalize_for_class(del_method, finalize_name, emitter) - emit_line() if cl.allow_interpreted_subclasses: shadow_vtable_name: str | None = generate_vtables( @@ -317,6 +314,9 @@ def emit_line() -> None: shadow_vtable_name = None vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter, shadow=False) emit_line() + if del_method: + generate_finalize_for_class(del_method, finalize_name, emitter) + emit_line() if needs_getseters: generate_getseter_declarations(cl, emitter) emit_line() diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 97bc063dd8ea..288f281c0a94 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2754,6 +2754,21 @@ def test_function(): assert(isinstance(d.fitem, ForwardDefinedClass)) assert(isinstance(d.fitems, ForwardDefinedClass)) +[case testDelForDictSubclass-xfail] +# The crash in issue mypy#19175 is fixed. +# But, for classes that derive from built-in Python classes, user-defined __del__ method is not +# being invoked. +class DictSubclass(dict): + def __del__(self): + print("deleting DictSubclass...") + +[file driver.py] +import native +native.DictSubclass() + +[out] +deleting DictSubclass... + [case testDel] class A: def __del__(self): @@ -2774,6 +2789,12 @@ class C(B): class D(A): pass +# Just make sure that this class compiles (see issue mypy#19175). testDelForDictSubclass tests for +# correct output. +class NormDict(dict): + def __del__(self) -> None: + pass + [file driver.py] import native native.C() From cb3c6ec6a7aaa96a0e26768a946ac63ea14115f2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 3 Jun 2025 18:25:02 +0100 Subject: [PATCH 4/9] Fix crash on partial type used as context (#19216) Fixes https://github.com/python/mypy/issues/19213 --- mypy/checker.py | 4 +++- test-data/unit/check-inference.test | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9c389cccd95f..cb465b390aa6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3426,7 +3426,9 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) -> # store the rvalue type on the variable. actual_lvalue_type = None if lvalue_node.is_inferred and not lvalue_node.explicit_self_type: - rvalue_type = self.expr_checker.accept(rvalue, lvalue_node.type) + # Don't use partial types as context, similar to regular code path. + ctx = lvalue_node.type if not isinstance(lvalue_node.type, PartialType) else None + rvalue_type = self.expr_checker.accept(rvalue, ctx) actual_lvalue_type = lvalue_node.type lvalue_node.type = rvalue_type lvalue_type, _ = self.node_type_from_base(lvalue_node.name, lvalue_node.info, lvalue) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 25565946158e..093924ab2e26 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3979,3 +3979,24 @@ def check(mapping: Mapping[str, _T]) -> None: reveal_type(ok1) # N: Revealed type is "Union[_T`-1, builtins.str]" ok2: Union[_T, str] = mapping.get("", "") [builtins fixtures/tuple.pyi] + +[case testNoCrashOnPartialTypeAsContext] +from typing import overload, TypeVar, Optional, Protocol + +T = TypeVar("T") +class DbManager(Protocol): + @overload + def get(self, key: str) -> Optional[T]: + pass + + @overload + def get(self, key: str, default: T) -> T: + pass + +class Foo: + def __init__(self, db: DbManager, bar: bool) -> None: + if bar: + self.qux = db.get("qux") + else: + self.qux = {} # E: Need type annotation for "qux" (hint: "qux: Dict[, ] = ...") +[builtins fixtures/dict.pyi] From c86480ce51e4bb6db21f4b3f0b3ec8833aafc8ce Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 1 Jun 2025 02:30:37 +0100 Subject: [PATCH 5/9] Tighten metaclass __call__ handling in protocols (#19191) Fixes https://github.com/python/mypy/issues/19184 This fixes an (edge-case) regression introduced in 1.16. Fix is straightforward, only ignore `__call__` if it comes from an _actual_ metaclass. --- mypy/constraints.py | 4 ++-- mypy/nodes.py | 4 ++-- mypy/typeops.py | 2 +- test-data/unit/check-protocols.test | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 8e7a30e05ffb..73b7ec7954a4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1066,8 +1066,8 @@ def infer_constraints_from_protocol_members( inst, erase_typevars(temp), ignore_pos_arg_names=True ): continue - # This exception matches the one in subtypes.py, see PR #14121 for context. - if member == "__call__" and instance.type.is_metaclass(): + # This exception matches the one in typeops.py, see PR #14121 for context. + if member == "__call__" and instance.type.is_metaclass(precise=True): continue res.extend(infer_constraints(temp, inst, self.direction)) if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol): diff --git a/mypy/nodes.py b/mypy/nodes.py index 2fb459142066..9a9403722a16 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3371,11 +3371,11 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None: return c return None - def is_metaclass(self) -> bool: + def is_metaclass(self, *, precise: bool = False) -> bool: return ( self.has_base("builtins.type") or self.fullname == "abc.ABCMeta" - or self.fallback_to_any + or (self.fallback_to_any and not precise) ) def has_base(self, fullname: str) -> bool: diff --git a/mypy/typeops.py b/mypy/typeops.py index bcf946900563..3715081ae173 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1257,7 +1257,7 @@ def named_type(fullname: str) -> Instance: return type_object_type(left.type, named_type) - if member == "__call__" and left.type.is_metaclass(): + if member == "__call__" and left.type.is_metaclass(precise=True): # Special case: we want to avoid falling back to metaclass __call__ # if constructor signature didn't match, this can cause many false negatives. return None diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 34e3f3e88080..56c3d72d23e2 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4460,3 +4460,25 @@ f2(a4) # E: Argument 1 to "f2" has incompatible type "A4"; expected "P2" \ # N: foo: expected "B1", got "str" \ # N: foo: expected setter type "C1", got "str" [builtins fixtures/property.pyi] + +[case testInferCallableProtoWithAnySubclass] +from typing import Any, Generic, Protocol, TypeVar + +T = TypeVar("T", covariant=True) + +Unknown: Any +class Mock(Unknown): + def __init__(self, **kwargs: Any) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + +class Factory(Protocol[T]): + def __call__(self, **kwargs: Any) -> T: ... + + +class Test(Generic[T]): + def __init__(self, f: Factory[T]) -> None: + ... + +t = Test(Mock()) +reveal_type(t) # N: Revealed type is "__main__.Test[Any]" +[builtins fixtures/dict.pyi] From c20fd7838338cd65d6c7c6e252eda85996cfc98e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 5 Jun 2025 15:09:36 +0100 Subject: [PATCH 6/9] Handle assignment of bound methods in class bodies (#19233) Fixes https://github.com/python/mypy/issues/18438 Fixes https://github.com/python/mypy/issues/19146 Surprisingly, a very small change is sufficient to replicate Python runtime behavior for all the important cases (see `checkmember.py`). I also replace the `bound_args` argument of `CallableType`, that was mostly unused, with a flag (as suggested by @JukkaL) and make sure it is properly set/preserved everywhere. --- mypy/checker.py | 2 +- mypy/checkexpr.py | 2 +- mypy/checkmember.py | 4 +-- mypy/fixup.py | 3 -- mypy/messages.py | 4 +-- mypy/server/astdiff.py | 1 + mypy/typeops.py | 3 +- mypy/types.py | 20 ++++++------- test-data/unit/check-classes.test | 2 +- test-data/unit/check-functions.test | 42 +++++++++++++++++++++++++++ test-data/unit/check-incremental.test | 24 +++++++++++++++ test-data/unit/fine-grained.test | 24 +++++++++++++++ 12 files changed, 110 insertions(+), 21 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cb465b390aa6..5a5d19cc0006 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2448,7 +2448,7 @@ def erase_override(t: Type) -> Type: if not is_subtype(original_arg_type, erase_override(override_arg_type)): context: Context = node if isinstance(node, FuncDef) and not node.is_property: - arg_node = node.arguments[i + len(override.bound_args)] + arg_node = node.arguments[i + override.bound()] if arg_node.line != -1: context = arg_node self.msg.argument_incompatible_with_supertype( diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f375c14c5fc4..df0e9bdaedde 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4970,7 +4970,7 @@ def apply_type_arguments_to_callable( tp.fallback, name="tuple", definition=tp.definition, - bound_args=tp.bound_args, + is_bound=tp.is_bound, ) self.msg.incompatible_type_application( min_arg_count, len(type_vars), len(args), ctx diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4821c622cb0c..ea76a6cc304f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -916,7 +916,7 @@ def analyze_var( bound_items = [] 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(): + if isinstance(p_ct, FunctionLike) and (not p_ct.bound() or var.is_property): 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) @@ -1503,6 +1503,6 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F: arg_types=method.arg_types[1:], arg_kinds=method.arg_kinds[1:], arg_names=method.arg_names[1:], - bound_args=[original_type], + is_bound=True, ) return cast(F, res) diff --git a/mypy/fixup.py b/mypy/fixup.py index 8e7cd40544bf..0e9c186fd42a 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -271,9 +271,6 @@ def visit_callable_type(self, ct: CallableType) -> None: ct.ret_type.accept(self) for v in ct.variables: v.accept(self) - for arg in ct.bound_args: - if arg: - arg.accept(self) if ct.type_guard is not None: ct.type_guard.accept(self) if ct.type_is is not None: diff --git a/mypy/messages.py b/mypy/messages.py index 2e07d7f63498..47e7d20dde78 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -644,8 +644,8 @@ def incompatible_argument( callee_name = callable_name(callee) if callee_name is not None: name = callee_name - if callee.bound_args and callee.bound_args[0] is not None: - base = format_type(callee.bound_args[0], self.options) + if object_type is not None: + base = format_type(object_type, self.options) else: base = extract_type(name) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 1b0cc218ed16..16a0d882a8aa 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -460,6 +460,7 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem: typ.is_type_obj(), typ.is_ellipsis_args, snapshot_types(typ.variables), + typ.is_bound, ) def normalize_callable_variables(self, typ: CallableType) -> CallableType: diff --git a/mypy/typeops.py b/mypy/typeops.py index 3715081ae173..1a29054513a2 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -185,6 +185,7 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=["_args", "_kwds"], ret_type=any_type, + is_bound=True, fallback=named_type("builtins.function"), ) return class_callable(sig, info, fallback, None, is_new=False) @@ -479,7 +480,7 @@ class B(A): pass arg_kinds=func.arg_kinds[1:], arg_names=func.arg_names[1:], variables=variables, - bound_args=[original_type], + is_bound=True, ) return cast(F, res) diff --git a/mypy/types.py b/mypy/types.py index 41a958ae93cc..7f67c303a929 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1597,6 +1597,9 @@ def with_name(self, name: str) -> FunctionLike: def get_name(self) -> str | None: pass + def bound(self) -> bool: + return bool(self.items) and self.items[0].is_bound + class FormalArgument(NamedTuple): name: str | None @@ -1826,8 +1829,7 @@ class CallableType(FunctionLike): # 'dict' and 'partial' for a `functools.partial` evaluation) "from_type_type", # Was this callable generated by analyzing Type[...] # instantiation? - "bound_args", # Bound type args, mostly unused but may be useful for - # tools that consume mypy ASTs + "is_bound", # Is this a bound method? "def_extras", # Information about original definition we want to serialize. # This is used for more detailed error messages. "type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case). @@ -1855,7 +1857,7 @@ def __init__( implicit: bool = False, special_sig: str | None = None, from_type_type: bool = False, - bound_args: Sequence[Type | None] = (), + is_bound: bool = False, def_extras: dict[str, Any] | None = None, type_guard: Type | None = None, type_is: Type | None = None, @@ -1888,9 +1890,7 @@ def __init__( self.from_type_type = from_type_type self.from_concatenate = from_concatenate self.imprecise_arg_kinds = imprecise_arg_kinds - if not bound_args: - bound_args = () - self.bound_args = bound_args + self.is_bound = is_bound if def_extras: self.def_extras = def_extras elif isinstance(definition, FuncDef): @@ -1927,7 +1927,7 @@ def copy_modified( implicit: Bogus[bool] = _dummy, special_sig: Bogus[str | None] = _dummy, from_type_type: Bogus[bool] = _dummy, - bound_args: Bogus[list[Type | None]] = _dummy, + is_bound: Bogus[bool] = _dummy, def_extras: Bogus[dict[str, Any]] = _dummy, type_guard: Bogus[Type | None] = _dummy, type_is: Bogus[Type | None] = _dummy, @@ -1952,7 +1952,7 @@ def copy_modified( implicit=implicit if implicit is not _dummy else self.implicit, special_sig=special_sig if special_sig is not _dummy else self.special_sig, from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type, - bound_args=bound_args if bound_args is not _dummy else self.bound_args, + is_bound=is_bound if is_bound is not _dummy else self.is_bound, def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras), type_guard=type_guard if type_guard is not _dummy else self.type_guard, type_is=type_is if type_is is not _dummy else self.type_is, @@ -2277,7 +2277,7 @@ def serialize(self) -> JsonDict: "variables": [v.serialize() for v in self.variables], "is_ellipsis_args": self.is_ellipsis_args, "implicit": self.implicit, - "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], + "is_bound": self.is_bound, "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, "type_is": (self.type_is.serialize() if self.type_is is not None else None), @@ -2300,7 +2300,7 @@ def deserialize(cls, data: JsonDict) -> CallableType: variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]], is_ellipsis_args=data["is_ellipsis_args"], implicit=data["implicit"], - bound_args=[(None if t is None else deserialize_type(t)) for t in data["bound_args"]], + is_bound=data["is_bound"], def_extras=data["def_extras"], type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 59204cc31c43..036c8e21c310 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -4292,7 +4292,7 @@ int.__eq__(3, 4) [builtins fixtures/args.pyi] [out] main:33: error: Too few arguments for "__eq__" of "int" -main:33: error: Unsupported operand types for == ("int" and "Type[int]") +main:33: error: Unsupported operand types for == ("Type[int]" and "Type[int]") [case testDupBaseClasses] class A: diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index ac93c6c20354..35d76a65ccc3 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3599,3 +3599,45 @@ class Bar(Foo): def foo(self, value: Union[int, str]) -> Union[int, str]: return super().foo(value) # E: Call to abstract method "foo" of "Foo" with trivial body via super() is unsafe + +[case testBoundMethodsAssignedInClassBody] +from typing import Callable + +class A: + def f(self, x: int) -> str: + pass + @classmethod + def g(cls, x: int) -> str: + pass + @staticmethod + def h(x: int) -> str: + pass + attr: Callable[[int], str] + +class C: + x1 = A.f + x2 = A.g + x3 = A().f + x4 = A().g + x5 = A.h + x6 = A().h + x7 = A().attr + +reveal_type(C.x1) # N: Revealed type is "def (self: __main__.A, x: builtins.int) -> builtins.str" +reveal_type(C.x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x7) # N: Revealed type is "def (builtins.int) -> builtins.str" + +reveal_type(C().x1) # E: Invalid self argument "C" to attribute function "x1" with type "Callable[[A, int], str]" \ + # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x7) # E: Invalid self argument "C" to attribute function "x7" with type "Callable[[int], str]" \ + # N: Revealed type is "def () -> builtins.str" +[builtins fixtures/classmethod.pyi] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 9d5902246ae5..fabd7e294d9a 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6862,3 +6862,27 @@ if int(): [out] [out2] main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testMethodMakeBoundIncremental] +from a import A +a = A() +a.f() +[file a.py] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = f +[file a.py.2] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = B().f +[out] +[out2] +main:3: error: Too few arguments diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index df244b3135e9..eb07f37a8671 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11217,3 +11217,27 @@ class A: [out] == main:3: error: Property "f" defined in "A" is read-only + +[case testMethodMakeBoundFineGrained] +from a import A +a = A() +a.f() +[file a.py] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = f +[file a.py.2] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = B().f +[out] +== +main:3: error: Too few arguments From 9fb5ff66c51bd971d7a6b1260cc0ec9f1b82cc06 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Jun 2025 23:59:23 +0100 Subject: [PATCH 7/9] Fix properties with setters after deleters (#19248) Fixes https://github.com/python/mypy/issues/19224 Note we must add an additional attribute on `OverloadedFuncDef` since decorator expressions are not serialized. --- mypy/checker.py | 10 ++++------ mypy/checkmember.py | 4 ++-- mypy/nodes.py | 24 +++++++++++++++++++++++- mypy/semanal.py | 1 + test-data/unit/check-classes.test | 20 ++++++++++++++++++++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5a5d19cc0006..dc3d670dd73e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -675,11 +675,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(defn.items[0], Decorator) self.visit_decorator(defn.items[0]) if defn.items[0].var.is_settable_property: - # TODO: here and elsewhere we assume setter immediately follows getter. - assert isinstance(defn.items[1], Decorator) # Perform a reduced visit just to infer the actual setter type. - self.visit_decorator_inner(defn.items[1], skip_first_item=True) - setter_type = defn.items[1].var.type + self.visit_decorator_inner(defn.setter, skip_first_item=True) + setter_type = defn.setter.var.type # Check if the setter can accept two positional arguments. any_type = AnyType(TypeOfAny.special_form) fallback_setter_type = CallableType( @@ -690,7 +688,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: fallback=self.named_type("builtins.function"), ) if setter_type and not is_subtype(setter_type, fallback_setter_type): - self.fail("Invalid property setter signature", defn.items[1].func) + self.fail("Invalid property setter signature", defn.setter.func) setter_type = self.extract_callable_type(setter_type, defn) if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2: # TODO: keep precise type for callables with tricky but valid signatures. @@ -2149,7 +2147,7 @@ def check_setter_type_override(self, defn: OverloadedFuncDef, base: TypeInfo) -> assert typ is not None and original_type is not None if not is_subtype(original_type, typ): - self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base) + self.msg.incompatible_setter_override(defn.setter, typ, original_type, base) def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ea76a6cc304f..596eccab09a7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -341,8 +341,8 @@ def analyze_instance_member_access( assert isinstance(method, OverloadedFuncDef) getter = method.items[0] assert isinstance(getter, Decorator) - if mx.is_lvalue and (len(items := method.items) > 1): - mx.chk.warn_deprecated(items[1], mx.context) + if mx.is_lvalue and getter.var.is_settable_property: + mx.chk.warn_deprecated(method.setter, mx.context) return analyze_var(name, getter.var, typ, mx) if mx.is_lvalue and not mx.suppress_errors: diff --git a/mypy/nodes.py b/mypy/nodes.py index 9a9403722a16..0d4c96ac5793 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -550,12 +550,20 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement): Overloaded variants must be consecutive in the source file. """ - __slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self") + __slots__ = ( + "items", + "unanalyzed_items", + "impl", + "deprecated", + "setter_index", + "_is_trivial_self", + ) items: list[OverloadPart] unanalyzed_items: list[OverloadPart] impl: OverloadPart | None deprecated: str | None + setter_index: int | None def __init__(self, items: list[OverloadPart]) -> None: super().__init__() @@ -563,6 +571,7 @@ def __init__(self, items: list[OverloadPart]) -> None: self.unanalyzed_items = items.copy() self.impl = None self.deprecated = None + self.setter_index = 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). @@ -598,6 +607,17 @@ def is_trivial_self(self) -> bool: self._is_trivial_self = True return True + @property + def setter(self) -> Decorator: + # Do some consistency checks first. + first_item = self.items[0] + assert isinstance(first_item, Decorator) + assert first_item.var.is_settable_property + assert self.setter_index is not None + item = self.items[self.setter_index] + assert isinstance(item, Decorator) + return item + def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_overloaded_func_def(self) @@ -610,6 +630,7 @@ def serialize(self) -> JsonDict: "impl": None if self.impl is None else self.impl.serialize(), "flags": get_flags(self, FUNCBASE_FLAGS), "deprecated": self.deprecated, + "setter_index": self.setter_index, } @classmethod @@ -630,6 +651,7 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef: res._fullname = data["fullname"] set_flags(res, data["flags"]) res.deprecated = data["deprecated"] + res.setter_index = data["setter_index"] # NOTE: res.info will be set in the fixup phase. return res diff --git a/mypy/semanal.py b/mypy/semanal.py index 89bb5ab97c2a..bc64e7e7ca53 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1558,6 +1558,7 @@ def analyze_property_with_multi_part_definition( ) assert isinstance(setter_func_type, CallableType) bare_setter_type = setter_func_type + defn.setter_index = i + 1 if first_node.name == "deleter": item.func.abstract_status = first_item.func.abstract_status for other_node in item.decorators[1:]: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 036c8e21c310..10e15139218f 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8739,3 +8739,23 @@ class NoopPowerResource: def hardware_type(self) -> None: # E: Invalid property setter signature self.hardware_type = None # Note: intentionally recursive [builtins fixtures/property.pyi] + +[case testPropertyAllowsDeleterBeforeSetter] +class C: + @property + def foo(self) -> str: ... + @foo.deleter + def foo(self) -> None: ... + @foo.setter + def foo(self, val: int) -> None: ... + + @property + def bar(self) -> int: ... + @bar.deleter + def bar(self) -> None: ... + @bar.setter + def bar(self, value: int, val: int) -> None: ... # E: Invalid property setter signature + +C().foo = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C().bar = "fine" +[builtins fixtures/property.pyi] From e253eded9c887630f3f5404c4b9f73f13570476a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 11 Jun 2025 13:57:31 +0900 Subject: [PATCH 8/9] Single underscore is not a sunder (#19273) Fixes https://github.com/python/mypy/issues/19271. --- mypy/util.py | 2 +- test-data/unit/check-enum.test | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mypy/util.py b/mypy/util.py index d3f49f74bbae..d7ff2a367fa2 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -66,7 +66,7 @@ def is_dunder(name: str, exclude_special: bool = False) -> bool: def is_sunder(name: str) -> bool: - return not is_dunder(name) and name.startswith("_") and name.endswith("_") + return not is_dunder(name) and name.startswith("_") and name.endswith("_") and name != "_" def split_module_names(mod_name: str) -> list[str]: diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index cc9048db18dc..3001bc9a69cb 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -2524,3 +2524,18 @@ class Base: self.o = Enum("o", names) # E: Enum type as attribute is not supported \ # E: Second argument of Enum() must be string, tuple, list or dict literal for mypy to determine Enum members [builtins fixtures/tuple.pyi] + +[case testSingleUnderscoreNameEnumMember] +# flags: --warn-unreachable + +# https://github.com/python/mypy/issues/19271 +from enum import Enum + +class Things(Enum): + _ = "under score" + +def check(thing: Things) -> None: + if thing is Things._: + return None + return None # E: Statement is unreachable +[builtins fixtures/enum.pyi] From 68b8fa097d080c92d30a429bc74de8acd56caf85 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 Jun 2025 16:25:56 +0100 Subject: [PATCH 9/9] Bump version to 1.16.1 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index d2d42a386dec..a003b2b70558 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.16.1+dev" +__version__ = "1.16.1" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))