From 3d6b6016c8c79a8a7c1d015b04d87a703078527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 15:46:39 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(tk):=20avoid=20display?= =?UTF-8?q?=20access=20during=20probing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python_discovery/_py_info_collect.py | 7 +++++-- tests/test_py_info_collect.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/python_discovery/_py_info_collect.py b/src/python_discovery/_py_info_collect.py index a1bdf2d..7764827 100644 --- a/src/python_discovery/_py_info_collect.py +++ b/src/python_discovery/_py_info_collect.py @@ -212,9 +212,12 @@ def _resolve_tk_lib(tcl, tcl_lib): # pragma: no cover tk_lib = PythonInfoCollector._query_tk_library(tcl) if tk_lib is not None: return tk_lib - tk_version = tcl.eval("package require Tk") + import _tkinter # ruff:ignore[import-outside-top-level,import-private-name] # novermin # linked Tk version + + # Loading Tk can block on DISPLAY; the linked version is enough to locate its library. + tk_version = _tkinter.TK_VERSION tcl_parent = os.path.dirname(tcl_lib) - for version in (tk_version, ".".join(tk_version.split(".")[:2]), tk_version.split(".")[0]): + for version in (tk_version, tk_version.split(".")[0]): tk_lib_path = os.path.join(tcl_parent, "tk{}".format(version)) if os.path.isdir(tk_lib_path) and os.path.exists(os.path.join(tk_lib_path, "tk.tcl")): return tk_lib_path diff --git a/tests/test_py_info_collect.py b/tests/test_py_info_collect.py index 5385cea..34a2d26 100644 --- a/tests/test_py_info_collect.py +++ b/tests/test_py_info_collect.py @@ -319,6 +319,28 @@ def test_tcl_tk_libs_queried_with_env(tmp_path: Path, mocker: MockerFixture) -> assert (info.tcl_lib, info.tk_lib) == ("/tcl-lib", str(tk_dir)) +@pytest.mark.parametrize("folder", [pytest.param("tk8.6", id="minor"), pytest.param("tk8", id="major")]) +def test_tk_lib_resolved_without_loading_tk(tmp_path: Path, mocker: MockerFixture, folder: str) -> None: + tcl_dir, tk_dir = tmp_path / "tcl8.6", tmp_path / folder + tk_dir.mkdir() + (tk_dir / "tk.tcl").touch() + + def evaluate(script: str) -> str: + if script == "info library": + return str(tcl_dir) + if script == "set tk_library": + raise _TclError(script) + raise AssertionError + + _fake_tkinter(mocker, evaluate) + mocker.patch.dict(sys.modules, {"_tkinter": mocker.MagicMock(TK_VERSION="8.6")}) + mocker.patch.dict(os.environ, {"TCL_LIBRARY": str(tcl_dir)}) + + info = PythonInfo() + + assert (info.tcl_lib, info.tk_lib) == (str(tcl_dir), str(tk_dir)) + + def test_tcl_tk_libs_none_on_tcl_error(tmp_path: Path, mocker: MockerFixture) -> None: _fake_tkinter(mocker, _TclError("fail")) mocker.patch.dict(os.environ, {"TCL_LIBRARY": str(tmp_path)}) From 4d34239e91a6a7901ceb823413f41ccb92433839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 15:48:02 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(news):=20note=20Tk=20di?= =?UTF-8?q?splay=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/211.security.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/changelog/211.security.rst diff --git a/docs/changelog/211.security.rst b/docs/changelog/211.security.rst new file mode 100644 index 0000000..d4ea828 --- /dev/null +++ b/docs/changelog/211.security.rst @@ -0,0 +1,2 @@ +Tcl/Tk library detection no longer opens an X display, so an unresponsive display cannot block interpreter discovery. +It can also find the Tk library on headless systems.