Skip to content

Commit 8818491

Browse files
zaniebotzanieb
andauthored
Fix Tcl 9 thread detection on Python 3.10–3.12 (#1321)
Python 3.10–3.12 treats Tcl 9 as non-threaded because Tcl 9 no longer exposes `tcl_platform(threaded)`. This bypasses `_tkinter`'s interpreter-thread dispatch, losing scheduled callbacks and potentially crashing on calls from worker threads. Backport python/cpython#128103 to these Unix builds so `_tkinter` treats Tcl 9 as always threaded. Fixes #1320. --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
1 parent 5974083 commit 8818491

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

‎cpython-unix/build-cpython.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,12 @@ if [ "${PYTHON_MAJMIN_VERSION}" = "3.11" ]; then
285285
patch -p1 -i "${ROOT}/patch-tkinter-backport-tcl-9-311.patch"
286286
fi
287287

288+
# Tcl 9 is always threaded and no longer exposes tcl_platform(threaded).
289+
# Backport https://github.com/python/cpython/pull/128103 (merged in 3.13+).
290+
if [ -n "${PYTHON_MEETS_MAXIMUM_VERSION_3_12}" ]; then
291+
patch -p1 -i "${ROOT}/patch-tkinter-tcl9-thread-detection.patch"
292+
fi
293+
288294
# BOLT instrumented binaries segfault in some test_embed tests for unknown reasons.
289295
# On 3.12 (minimum BOLT version), the segfault causes the test harness to
290296
# abort and BOLT optimization uses the partial test results. On 3.13, the segfault
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c
2+
--- a/Modules/_tkinter.c
3+
+++ b/Modules/_tkinter.c
4+
@@ -587,6 +587,10 @@ Tkapp_New(const char *screenName, const char *className,
5+
v->interp = Tcl_CreateInterp();
6+
v->wantobjects = wantobjects;
7+
+#if TCL_MAJOR_VERSION >= 9
8+
+ v->threaded = 1;
9+
+#else
10+
v->threaded = Tcl_GetVar2Ex(v->interp, "tcl_platform", "threaded",
11+
TCL_GLOBAL_ONLY) != NULL;
12+
+#endif
13+
v->thread_id = Tcl_GetCurrentThread();
14+
v->dispatching = 0;

‎pythonbuild/disttests/__init__.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,41 @@ def test_windows_tcltk(self):
326326
interpreter.eval("zlib decompress [zlib compress test]"), "test"
327327
)
328328

329+
@unittest.skipIf(
330+
os.name == "nt" and "static" in os.environ["BUILD_OPTIONS"],
331+
"Tcl/Tk is unavailable in Windows static builds",
332+
)
333+
def test_tcl_thread_dispatch(self):
334+
# Tcl calls from another thread must wait for the interpreter's main loop.
335+
# Use a subprocess so a broken dispatcher cannot crash or hang the suite.
336+
probe = """
337+
import threading
338+
import tkinter
339+
340+
interpreter = tkinter.Tcl()
341+
errors = []
342+
343+
def worker():
344+
try:
345+
interpreter.call("set", "thread_dispatch_probe", "value")
346+
except RuntimeError as exc:
347+
errors.append(str(exc))
348+
349+
thread = threading.Thread(target=worker, daemon=True)
350+
thread.start()
351+
thread.join(5)
352+
assert not thread.is_alive(), "Tcl call did not finish"
353+
assert errors == ["main thread is not in main loop"], errors
354+
"""
355+
result = subprocess.run(
356+
[sys.executable, "-c", probe],
357+
stdout=subprocess.PIPE,
358+
stderr=subprocess.STDOUT,
359+
text=True,
360+
timeout=10,
361+
)
362+
self.assertEqual(result.returncode, 0, result.stdout)
363+
329364
@unittest.skipIf("TCL_LIBRARY" not in os.environ, "TCL_LIBRARY not set")
330365
@unittest.skipIf("DISPLAY" not in os.environ, "DISPLAY not set")
331366
def test_tkinter(self):

0 commit comments

Comments
 (0)