[med-svn] [Git][python-team/packages/mypy][debian/1.20.2-2] 2 commits: Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.
Michael R. Crusoe (@crusoe)
gitlab at salsa.debian.org
Wed Apr 29 07:58:27 BST 2026
Michael R. Crusoe pushed to tag debian/1.20.2-2 at Debian Python Team / packages / mypy
Commits:
f9b76c5c by Michael R. Crusoe at 2026-04-23T18:24:25+02:00
Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.
- - - - -
d319951d by Michael R. Crusoe at 2026-04-29T08:57:53+02:00
Release to unstable
- - - - -
3 changed files:
- debian/changelog
- + debian/patches/0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
- debian/patches/series
Changes:
=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+mypy (1.20.2-2) unstable; urgency=medium
+
+ * Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.
+
+ -- Michael R. Crusoe <crusoe at debian.org> Wed, 29 Apr 2026 08:53:34 +0200
+
mypy (1.20.2-1) unstable; urgency=medium
* New upstream version
=====================================
debian/patches/0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
=====================================
@@ -0,0 +1,453 @@
+From ca800ef729855f8b73d561c34497ce0c91172376 Mon Sep 17 00:00:00 2001
+From: Piotr Sawicki <sawickipiotr at outlook.com>
+Date: Thu, 23 Apr 2026 16:03:11 +0200
+Subject: [mypyc] Generate more type methods for types with managed dicts (#21290)
+Origin: upstream, https://github.com/python/mypy/commit/c56ab5857047edac41db0826b2f83c5a3d476bc0
+
+Fixes #21133
+
+Types with the `Py_TPFLAGS_MANAGED_DICT` flag must call
+`PyObject_VisitManagedDict` and `PyObject_ClearManagedDict` in their
+`tp_traverse` / `tp_clear` functions according to
+[docs](https://docs.python.org/3/c-api/typeobj.html#c.Py_TPFLAGS_MANAGED_DICT)
+but the types generated by mypyc currently don't do this. We don't
+generate these functions at all so they get inherited from the base
+class.
+
+Failure to call these functions may result in a segfault in python 3.14
+when accessing the managed dict after its owner has been deallocated. I
+believe the crash happens because the logic for types with the
+`Py_TPFLAGS_INLINE_VALUES` flag in
+[`PyObject_ClearManagedDict`](https://github.com/python/cpython/blob/main/Objects/dictobject.c#L7803)
+is not run. The condition to add this flag has changed in
+[3.14](https://github.com/python/cpython/blob/3.14/Objects/typeobject.c#L8877)
+vs
+[3.13](https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L8171)
+so generated types with `Py_TPFLAGS_MANAGED_DICT` get it in 3.14.
+
+To fix, generate `tp_clear`, `tp_traverse`, and `tp_dealloc` for types
+with managed dicts. Also add a special case in these functions for
+classes with built-in bases to call the pointer of the base class.
+--- mypy.orig/mypyc/codegen/emit.py
++++ mypy/mypyc/codegen/emit.py
+@@ -1398,6 +1398,12 @@
+ self.emit_line(error_stmt)
+ return wrapper_name
+
++ def emit_base_tp_function_call(
++ self, derived_cl: ClassIR, tp_func: str, args: str, *, prefix: str = ""
++ ) -> None:
++ type_obj = self.type_struct_name(derived_cl)
++ self.emit_line(f"{prefix}{type_obj}->tp_base->{tp_func}({args});")
++
+
+ def c_array_initializer(components: list[str], *, indented: bool = False) -> str:
+ """Construct an initializer for a C array variable.
+--- mypy.orig/mypyc/codegen/emitclass.py
++++ mypy/mypyc/codegen/emitclass.py
+@@ -262,7 +262,8 @@
+ if not cl.builtin_base:
+ fields["tp_new"] = new_name
+
+- if generate_full:
++ managed_dict = has_managed_dict(cl, emitter)
++ if generate_full or managed_dict:
+ fields["tp_dealloc"] = f"(destructor){name_prefix}_dealloc"
+ if not cl.is_acyclic:
+ fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse"
+@@ -335,6 +336,14 @@
+ else:
+ fields["tp_basicsize"] = base_size
+
++ if generate_full or managed_dict:
++ if not cl.is_acyclic:
++ generate_traverse_for_class(cl, traverse_name, emitter)
++ emit_line()
++ generate_clear_for_class(cl, clear_name, emitter)
++ emit_line()
++ generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter)
++ emit_line()
+ if generate_full:
+ assert cl.setup is not None
+ emitter.emit_line(native_function_header(cl.setup, emitter) + ";")
+@@ -345,13 +354,6 @@
+ init_fn = cl.get_method("__init__")
+ generate_new_for_class(cl, new_name, vtable_name, setup_name, init_fn, emitter)
+ emit_line()
+- if not cl.is_acyclic:
+- generate_traverse_for_class(cl, traverse_name, emitter)
+- emit_line()
+- generate_clear_for_class(cl, clear_name, emitter)
+- emit_line()
+- generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter)
+- emit_line()
+
+ if cl.allow_interpreted_subclasses:
+ shadow_vtable_name: str | None = generate_vtables(
+@@ -380,7 +382,7 @@
+ emit_line()
+
+ flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"]
+- if generate_full and not cl.is_acyclic:
++ if (generate_full or managed_dict) and not cl.is_acyclic:
+ flags.append("Py_TPFLAGS_HAVE_GC")
+ if cl.has_method("__call__"):
+ fields["tp_vectorcall_offset"] = "offsetof({}, vectorcall)".format(
+@@ -391,7 +393,7 @@
+ # This is just a placeholder to please CPython. It will be
+ # overridden during setup.
+ fields["tp_call"] = "PyVectorcall_Call"
+- if has_managed_dict(cl, emitter):
++ if managed_dict:
+ flags.append("Py_TPFLAGS_MANAGED_DICT")
+ fields["tp_flags"] = " | ".join(flags)
+
+@@ -867,8 +869,14 @@
+ for base in reversed(cl.base_mro):
+ for attr, rtype in base.attributes.items():
+ emitter.emit_gc_visit(f"self->{emitter.attr(attr)}", rtype)
++ base_args = "(PyObject *)self, visit, arg"
++ emitter.emit_line("int rv = 0;")
++ if cl.builtin_base:
++ emitter.emit_base_tp_function_call(cl, "tp_traverse", base_args, prefix="rv = ")
++ emitter.emit_line("if (rv != 0) return rv;")
+ if has_managed_dict(cl, emitter):
+- emitter.emit_line("PyObject_VisitManagedDict((PyObject *)self, visit, arg);")
++ emitter.emit_line(f"rv = PyObject_VisitManagedDict({base_args});")
++ emitter.emit_line("if (rv != 0) return rv;")
+ elif cl.has_dict:
+ struct_name = cl.struct_name(emitter.names)
+ # __dict__ lives right after the struct and __weakref__ lives right after that
+@@ -879,7 +887,7 @@
+ f"*((PyObject **)((char *)self + sizeof(PyObject *) + sizeof({struct_name})))",
+ object_rprimitive,
+ )
+- emitter.emit_line("return 0;")
++ emitter.emit_line("return rv;")
+ emitter.emit_line("}")
+
+
+@@ -890,8 +898,11 @@
+ for base in reversed(cl.base_mro):
+ for attr, rtype in base.attributes.items():
+ emitter.emit_gc_clear(f"self->{emitter.attr(attr)}", rtype)
++ base_args = "(PyObject *)self"
++ if cl.builtin_base:
++ emitter.emit_base_tp_function_call(cl, "tp_clear", base_args)
+ if has_managed_dict(cl, emitter):
+- emitter.emit_line("PyObject_ClearManagedDict((PyObject *)self);")
++ emitter.emit_line(f"PyObject_ClearManagedDict({base_args});")
+ elif cl.has_dict:
+ struct_name = cl.struct_name(emitter.names)
+ # __dict__ lives right after the struct and __weakref__ lives right after that
+@@ -935,6 +946,18 @@
+ emitter.emit_line("}")
+ if not cl.is_acyclic:
+ emitter.emit_line("PyObject_GC_UnTrack(self);")
++ if cl.builtin_base:
++ emitter.emit_line(f"{clear_func_name}(self);")
++ # For native subclasses of builtins such as dict, the base deallocator
++ # is responsible for tearing down base-owned storage and freeing memory.
++ # Re-track self if base is GC-aware to match cpython's subtype_dealloc.
++ base = f"{emitter.type_struct_name(cl)}->tp_base"
++ base_arg = "(PyObject *)self"
++ emitter.emit_line(f"if (PyType_IS_GC({base})) PyObject_GC_Track({base_arg});")
++ emitter.emit_base_tp_function_call(cl, "tp_dealloc", base_arg)
++ emitter.emit_line("done: ;")
++ emitter.emit_line("}")
++ return
+ if cl.reuse_freed_instance:
+ emit_reuse_dealloc(cl, emitter)
+ # The trashcan is needed to handle deep recursive deallocations
+--- mypy.orig/mypyc/irbuild/prepare.py
++++ mypy/mypyc/irbuild/prepare.py
+@@ -372,22 +372,6 @@
+ if attrs.get("acyclic") is True:
+ ir.is_acyclic = True
+
+- free_list_len = attrs.get("free_list_len")
+- if free_list_len is not None:
+- line = attrs_lines["free_list_len"]
+- if ir.is_trait:
+- errors.error('"free_list_len" can\'t be used with traits', path, line)
+- if ir.allow_interpreted_subclasses:
+- errors.error(
+- '"free_list_len" can\'t be used in a class that allows interpreted subclasses',
+- path,
+- line,
+- )
+- if free_list_len == 1:
+- ir.reuse_freed_instance = True
+- else:
+- errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
+-
+ # Check for subclassing from builtin types
+ for cls in info.mro:
+ # Special case exceptions and dicts
+@@ -416,6 +400,28 @@
+ cdef.line,
+ )
+
++ free_list_len = attrs.get("free_list_len")
++ if free_list_len is not None:
++ line = attrs_lines["free_list_len"]
++ if ir.is_trait:
++ errors.error('"free_list_len" can\'t be used with traits', path, line)
++ if ir.allow_interpreted_subclasses:
++ errors.error(
++ '"free_list_len" can\'t be used in a class that allows interpreted subclasses',
++ path,
++ line,
++ )
++ if ir.builtin_base:
++ errors.error(
++ '"free_list_len" can\'t be used in a class that inherits from a built-in type',
++ path,
++ line,
++ )
++ if free_list_len == 1:
++ ir.reuse_freed_instance = True
++ else:
++ errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
++
+ # Set up the parent class
+ bases = [mapper.type_to_ir[base.type] for base in info.bases if base.type in mapper.type_to_ir]
+ if len(bases) > 1 and any(not c.is_trait for c in bases) and bases[0].is_trait:
+--- mypy.orig/mypyc/lib-rt/pythoncapi_compat.h
++++ mypy/mypyc/lib-rt/pythoncapi_compat.h
+@@ -925,7 +925,8 @@
+ {
+ PyObject **dict = _PyObject_GetDictPtr(obj);
+ if (dict == NULL || *dict == NULL) {
+- return -1;
++ // Nothing to do.
++ return 0;
+ }
+ Py_VISIT(*dict);
+ return 0;
+--- mypy.orig/mypyc/test-data/fixtures/ir.py
++++ mypy/mypyc/test-data/fixtures/ir.py
+@@ -40,6 +40,7 @@
+
+ class object:
+ __class__: type
++ __dict__: dict[str, Any]
+ def __new__(cls) -> Self: pass
+ def __init__(self) -> None: pass
+ def __init_subclass__(cls, **kwargs: object) -> None: pass
+--- mypy.orig/mypyc/test-data/irbuild-classes.test
++++ mypy/mypyc/test-data/irbuild-classes.test
+@@ -2138,6 +2138,10 @@
+ class InterpSub:
+ pass
+
++ at mypyc_attr(free_list_len=1) # E: "free_list_len" can't be used in a class that inherits from a built-in type
++class InheritsBuiltIn(dict):
++ pass
++
+ [case testUnsupportedGetAttr]
+ from mypy_extensions import mypyc_attr
+
+--- mypy.orig/mypyc/test-data/run-classes.test
++++ mypy/mypyc/test-data/run-classes.test
+@@ -3325,20 +3325,39 @@
+ 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.
++[case testDelForDictSubclass]
++events: list[str] = []
++
++class Item:
++ def __del__(self) -> None:
++ events.append("deleting Item")
++
+ class DictSubclass(dict):
+- def __del__(self):
+- print("deleting DictSubclass...")
++ def __del__(self) -> None:
++ events.append("deleting DictSubclass")
++
++def test_dict_subclass_dealloc() -> None:
++ d = DictSubclass()
++ d["item"] = Item()
++ del d
+
+ [file driver.py]
+-import native
+-native.DictSubclass()
++import sys
++
++from native import events, test_dict_subclass_dealloc
++
++test_dict_subclass_dealloc()
++
++expected_events: list[str] = []
++
++# TODO: Fix when compiling for older python.
++# The user-defined __del__ method is currently only invoked when __dict__ is a managed dict
++# because calling __del__ in tp_clear on older python crashes.
++if sys.version_info >= (3, 12):
++ expected_events.append("deleting DictSubclass")
++expected_events.append("deleting Item")
+
+-[out]
+-deleting DictSubclass...
++assert events == expected_events, events
+
+ [case testDel]
+ class A:
+--- mypy.orig/mypyc/test-data/run-dicts.test
++++ mypy/mypyc/test-data/run-dicts.test
+@@ -368,3 +368,148 @@
+ [file userdefineddict.py]
+ class dict:
+ pass
++
++[case testDunderDictAccessAfterDel]
++from mypy_extensions import mypyc_attr
++
++ at mypyc_attr(allow_interpreted_subclasses=True)
++class NormDict(dict[str, str]):
++ def __init__(self, attr: int = 42) -> None:
++ super().__init__()
++ self.attr = attr
++
++class SubNormDict(NormDict):
++ def __init__(self, attr: int = 43) -> None:
++ super().__init__(attr)
++
++def test_dict_access() -> None:
++ n = NormDict(1)
++ d = n.__dict__
++ assert d["attr"] == 1
++ del n
++ assert d["attr"] == 1
++
++def test_subclass_dict_access() -> None:
++ s = SubNormDict(1)
++ d = s.__dict__
++ assert d["attr"] == 1
++ del s
++ assert d["attr"] == 1
++
++[file driver.py]
++from native import NormDict, SubNormDict, test_dict_access, test_subclass_dict_access
++
++
++class InterpretedNormDict(NormDict):
++ pass
++
++def test_dict_access_interpreted() -> None:
++ n = NormDict()
++ d = n.__dict__
++ assert d["attr"] == 42
++ del n
++ assert d["attr"] == 42
++
++def test_subclass_dict_access_interpreted() -> None:
++ s = SubNormDict()
++ d = s.__dict__
++ assert d["attr"] == 43
++ del s
++ assert d["attr"] == 43
++
++def test_allow_interpreted_subclass_dict_access() -> None:
++ s = InterpretedNormDict()
++ d = s.__dict__
++ assert d["attr"] == 42
++ del s
++ assert d["attr"] == 42
++
++test_dict_access()
++test_dict_access_interpreted()
++
++test_subclass_dict_access()
++test_subclass_dict_access_interpreted()
++test_allow_interpreted_subclass_dict_access()
++
++[fixture fixtures/typing-full.pyi]
++
++[case testCycleInDictSubclass]
++import gc
++from mypy_extensions import mypyc_attr
++
++events: list[str] = []
++
++ at mypyc_attr(allow_interpreted_subclasses=True)
++class CyclicDict(dict):
++ def __init__(self) -> None:
++ self["self"] = self
++
++ def __del__(self) -> None:
++ events.append("deleted")
++
++class SubCyclicDict(CyclicDict):
++ def __init__(self) -> None:
++ super().__init__()
++
++ def __del__(self) -> None:
++ events.append("sub deleted")
++
++def test_cyclic_dict_cleanup() -> None:
++ global events
++ events = []
++
++ c = CyclicDict()
++ del c
++ gc.collect()
++ assert events == ["deleted"], events
++
++def test_sub_cyclic_dict_cleanup() -> None:
++ global events
++ events = []
++
++ c = SubCyclicDict()
++ del c
++ gc.collect()
++ assert events == ["sub deleted"], events
++
++[file driver.py]
++import gc
++
++import native
++from native import CyclicDict, SubCyclicDict, test_cyclic_dict_cleanup, test_sub_cyclic_dict_cleanup
++
++
++class InterpretedCyclicDict(CyclicDict):
++ def __del__(self) -> None:
++ native.events.append("interpreted deleted")
++
++def test_cyclic_dict_cleanup_interpreted() -> None:
++ native.events = []
++
++ c = CyclicDict()
++ del c
++ gc.collect()
++ assert native.events == ["deleted"], events
++
++def test_sub_cyclic_dict_cleanup_interpreted() -> None:
++ native.events = []
++
++ c = SubCyclicDict()
++ del c
++ gc.collect()
++ assert native.events == ["sub deleted"], events
++
++def test_allow_interpreted_subclass_cyclic_dict_cleanup() -> None:
++ native.events = []
++
++ c = InterpretedCyclicDict()
++ del c
++ gc.collect()
++ assert native.events == ["interpreted deleted"], events
++
++test_cyclic_dict_cleanup()
++test_sub_cyclic_dict_cleanup()
++
++test_cyclic_dict_cleanup_interpreted()
++test_sub_cyclic_dict_cleanup_interpreted()
++test_allow_interpreted_subclass_cyclic_dict_cleanup()
=====================================
debian/patches/series
=====================================
@@ -1,3 +1,4 @@
+0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
remove-mypyc-test-run-timeout
hint-typeshed-package
verbose
View it on GitLab: https://salsa.debian.org/python-team/packages/mypy/-/compare/9de387ae0e1233aed7dc146c09589daa832dee46...d319951d658671d7f5300f4e17ada6f1bf581f14
--
View it on GitLab: https://salsa.debian.org/python-team/packages/mypy/-/compare/9de387ae0e1233aed7dc146c09589daa832dee46...d319951d658671d7f5300f4e17ada6f1bf581f14
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20260429/f2849ade/attachment-0001.htm>
More information about the debian-med-commit
mailing list