Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/211.security.rst
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 5 additions & 2 deletions src/python_discovery/_py_info_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/test_py_info_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down