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
1 change: 1 addition & 0 deletions docs/changelog/208.security.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tcl/Tk library detection no longer executes Tk startup profile files from the home or working directory.
7 changes: 6 additions & 1 deletion src/python_discovery/_py_info_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,13 @@ def _get_tcl_tk_libs(): # pragma: no cover # tkinter availability varies; teste
except ImportError:
pass
else:
# Tk.__init__ reads profile scripts from HOME or the working directory; probing needs only library metadata.
class _TclWithoutProfile(tk.Tk):
def readprofile(self, *_args, **_kwargs): # ruff:ignore[no-self-use] # Tk override must be an instance method.
return None

try:
tcl = tk.Tcl()
tcl = _TclWithoutProfile(useTk=False)
tcl_lib = tcl.eval("info library")
tk_lib = PythonInfoCollector._resolve_tk_lib(tcl, tcl_lib)
except tk.TclError:
Expand Down
24 changes: 22 additions & 2 deletions tests/test_py_info_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

if TYPE_CHECKING:
from collections.abc import Callable
from unittest.mock import Mock

from pytest_mock import MockerFixture

Expand Down Expand Up @@ -304,9 +305,16 @@ class _TclError(Exception):
"""Stands in for tkinter.TclError; an except clause needs a real exception type."""


def _fake_tkinter(mocker: MockerFixture, eval_side_effect: object) -> None:
module = mocker.MagicMock(TclError=_TclError, **{"Tcl.return_value.eval.side_effect": eval_side_effect})
def _fake_tkinter(mocker: MockerFixture, eval_side_effect: Callable[[str], str] | Exception) -> Mock:
evaluate = mocker.Mock(side_effect=eval_side_effect)
profile = mocker.Mock(return_value=None)
interpreter = type(
"Tk", (), {"__init__": lambda self, **_: self.readprofile("Tk", "Tk"), "readprofile": profile, "eval": evaluate}
)
module = mocker.MagicMock(TclError=_TclError, Tk=interpreter)
module.Tcl.side_effect = lambda: interpreter(useTk=False)
mocker.patch.dict(sys.modules, {"tkinter": module})
return profile


def test_tcl_tk_libs_queried_with_env(tmp_path: Path, mocker: MockerFixture) -> None:
Expand All @@ -326,6 +334,18 @@ def test_tcl_tk_libs_none_on_tcl_error(tmp_path: Path, mocker: MockerFixture) ->
assert (info.tcl_lib, info.tk_lib) == (None, None)


def test_tcl_tk_libs_skip_tk_profile(tmp_path: Path, mocker: MockerFixture) -> None:
profile = _fake_tkinter(mocker, {"info library": "/tcl-lib"}.__getitem__)
profile.side_effect = AssertionError("Tk profile loaded")
mocker.patch("python_discovery._py_info_collect.PythonInfoCollector._resolve_tk_lib", return_value=None)
mocker.patch.dict(os.environ, {"TCL_LIBRARY": str(tmp_path)})

info = PythonInfo()

assert info.tcl_lib == "/tcl-lib"
profile.assert_not_called()


def test_system_stdlib_empty_when_scheme_lacks_path(mocker: MockerFixture) -> None:
names = tuple(name for name in sysconfig.get_path_names() if name not in {"stdlib", "platstdlib"})
mocker.patch.object(sysconfig, "get_path_names", return_value=names)
Expand Down