diff --git a/docs/changelog/425.feature.rst b/docs/changelog/425.feature.rst new file mode 100644 index 000000000..aed60e75d --- /dev/null +++ b/docs/changelog/425.feature.rst @@ -0,0 +1,2 @@ +Added support for Tcl and Tkinter. You're welcome. +Contributed by :user:`esafak`. diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index d3cf34784..4ffeb8257 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -23,6 +23,17 @@ deactivate () { unset _OLD_VIRTUAL_PYTHONHOME fi + if ! [ -z "${_OLD_VIRTUAL_TCL_LIBRARY+_}" ]; then + TCL_LIBRARY="$_OLD_VIRTUAL_TCL_LIBRARY" + export TCL_LIBRARY + unset _OLD_VIRTUAL_TCL_LIBRARY + fi + if ! [ -z "${_OLD_VIRTUAL_TK_LIBRARY+_}" ]; then + TK_LIBRARY="$_OLD_VIRTUAL_TK_LIBRARY" + export TK_LIBRARY + unset _OLD_VIRTUAL_TK_LIBRARY + fi + # The hash command must be called to get it to forget past # commands. Without forgetting past commands the $PATH changes # we made may not be respected @@ -68,6 +79,22 @@ if ! [ -z "${PYTHONHOME+_}" ] ; then unset PYTHONHOME fi +if [ __TCL_LIBRARY__ != "''" ]; then + if ! [ -z "${TCL_LIBRARY+_}" ] ; then + _OLD_VIRTUAL_TCL_LIBRARY="$TCL_LIBRARY" + fi + TCL_LIBRARY=__TCL_LIBRARY__ + export TCL_LIBRARY +fi + +if [ __TK_LIBRARY__ != "''" ]; then + if ! [ -z "${TK_LIBRARY+_}" ] ; then + _OLD_VIRTUAL_TK_LIBRARY="$TK_LIBRARY" + fi + TK_LIBRARY=__TK_LIBRARY__ + export TK_LIBRARY +fi + if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then _OLD_VIRTUAL_PS1="${PS1-}" PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}" diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat index 36b0a8bd7..62f393c80 100644 --- a/src/virtualenv/activation/batch/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -33,6 +33,12 @@ @set PYTHONHOME= +@if defined TCL_LIBRARY @set "_OLD_VIRTUAL_TCL_LIBRARY=%TCL_LIBRARY%" +@if NOT "__TCL_LIBRARY__"=="" @set "TCL_LIBRARY=__TCL_LIBRARY__" + +@if defined TK_LIBRARY @set "_OLD_VIRTUAL_TK_LIBRARY=%TK_LIBRARY%" +@if NOT "__TK_LIBRARY__"=="" @set "TK_LIBRARY=__TK_LIBRARY__" + @REM if defined _OLD_VIRTUAL_PATH ( @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH1 @set "PATH=%_OLD_VIRTUAL_PATH%" diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat index 8939c6c0d..7a12d47ed 100644 --- a/src/virtualenv/activation/batch/deactivate.bat +++ b/src/virtualenv/activation/batch/deactivate.bat @@ -12,6 +12,14 @@ @set _OLD_VIRTUAL_PYTHONHOME= :ENDIFVHOME +@if defined _OLD_VIRTUAL_TCL_LIBRARY @set "TCL_LIBRARY=%_OLD_VIRTUAL_TCL_LIBRARY%" +@if not defined _OLD_VIRTUAL_TCL_LIBRARY @set TCL_LIBRARY= +@set _OLD_VIRTUAL_TCL_LIBRARY= + +@if defined _OLD_VIRTUAL_TK_LIBRARY @set "TK_LIBRARY=%_OLD_VIRTUAL_TK_LIBRARY%" +@if not defined _OLD_VIRTUAL_TK_LIBRARY @set TK_LIBRARY= +@set _OLD_VIRTUAL_TK_LIBRARY= + @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH @set "PATH=%_OLD_VIRTUAL_PATH%" @set _OLD_VIRTUAL_PATH= diff --git a/src/virtualenv/activation/cshell/activate.csh b/src/virtualenv/activation/cshell/activate.csh index 24de5508b..5c02616d7 100644 --- a/src/virtualenv/activation/cshell/activate.csh +++ b/src/virtualenv/activation/cshell/activate.csh @@ -5,7 +5,7 @@ set newline='\ ' -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_TCL_LIBRARY != 0 && setenv TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY:q" && unset _OLD_VIRTUAL_TCL_LIBRARY || unsetenv TCL_LIBRARY; test $?_OLD_VIRTUAL_TK_LIBRARY != 0 && setenv TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY:q" && unset _OLD_VIRTUAL_TK_LIBRARY || unsetenv TK_LIBRARY; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' # Unset irrelevant variables. deactivate nondestructive @@ -15,7 +15,19 @@ setenv VIRTUAL_ENV __VIRTUAL_ENV__ set _OLD_VIRTUAL_PATH="$PATH:q" setenv PATH "$VIRTUAL_ENV:q/"__BIN_NAME__":$PATH:q" +if (__TCL_LIBRARY__ != "") then + if ($?TCL_LIBRARY) then + set _OLD_VIRTUAL_TCL_LIBRARY="$TCL_LIBRARY" + endif + setenv TCL_LIBRARY __TCL_LIBRARY__ +endif +if (__TK_LIBRARY__ != "") then + if ($?TK_LIBRARY) then + set _OLD_VIRTUAL_TK_LIBRARY="$TK_LIBRARY" + endif + setenv TK_LIBRARY __TK_LIBRARY__ +endif if (__VIRTUAL_PROMPT__ != "") then setenv VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py index 57f790f47..28052e64f 100644 --- a/src/virtualenv/activation/fish/__init__.py +++ b/src/virtualenv/activation/fish/__init__.py @@ -7,6 +7,14 @@ class FishActivator(ViaTemplateActivator): def templates(self): yield "activate.fish" + def replacements(self, creator, dest): + data = super().replacements(creator, dest) + data.update({ + "__TCL_LIBRARY__": creator.interpreter.tcl_lib or "", + "__TK_LIBRARY__": creator.interpreter.tk_lib or "", + }) + return data + __all__ = [ "FishActivator", diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish index 518bc4cff..c9d174997 100644 --- a/src/virtualenv/activation/fish/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -26,6 +26,23 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen set -e _OLD_VIRTUAL_PATH end + if test -n __TCL_LIBRARY__ + if test -n "$_OLD_VIRTUAL_TCL_LIBRARY"; + set -gx TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY"; + set -e _OLD_VIRTUAL_TCL_LIBRARY; + else; + set -e TCL_LIBRARY; + end + end + if test -n __TK_LIBRARY__ + if test -n "$_OLD_VIRTUAL_TK_LIBRARY"; + set -gx TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY"; + set -e _OLD_VIRTUAL_TK_LIBRARY; + else; + set -e TK_LIBRARY; + end + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" set -e _OLD_VIRTUAL_PYTHONHOME @@ -68,6 +85,19 @@ else end set -gx PATH "$VIRTUAL_ENV"'/'__BIN_NAME__ $PATH +if test -n __TCL_LIBRARY__ + if set -q TCL_LIBRARY; + set -gx _OLD_VIRTUAL_TCL_LIBRARY $TCL_LIBRARY; + end + set -gx TCL_LIBRARY '__TCL_LIBRARY__' +end +if test -n __TK_LIBRARY__ + if set -q TK_LIBRARY; + set -gx _OLD_VIRTUAL_TK_LIBRARY $TK_LIBRARY; + end + set -gx TK_LIBRARY '__TK_LIBRARY__' +end + # Prompt override provided? # If not, just use the environment name. if test -n __VIRTUAL_PROMPT__ diff --git a/src/virtualenv/activation/nushell/__init__.py b/src/virtualenv/activation/nushell/__init__.py index ef7a79a9c..9558a70a5 100644 --- a/src/virtualenv/activation/nushell/__init__.py +++ b/src/virtualenv/activation/nushell/__init__.py @@ -12,6 +12,8 @@ def quote(string): """ Nushell supports raw strings like: r###'this is a string'###. + https://github.com/nushell/nushell.github.io/blob/main/book/working_with_strings.md + This method finds the maximum continuous sharps in the string and then quote it with an extra sharp. """ @@ -32,6 +34,8 @@ def replacements(self, creator, dest_folder): # noqa: ARG002 "__VIRTUAL_ENV__": str(creator.dest), "__VIRTUAL_NAME__": creator.env_name, "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), + "__TCL_LIBRARY__": creator.interpreter.tcl_lib or "", + "__TK_LIBRARY__": creator.interpreter.tk_lib or "", } diff --git a/src/virtualenv/activation/nushell/activate.nu b/src/virtualenv/activation/nushell/activate.nu index 7f43071f7..b48fdd03f 100644 --- a/src/virtualenv/activation/nushell/activate.nu +++ b/src/virtualenv/activation/nushell/activate.nu @@ -58,6 +58,12 @@ export-env { __VIRTUAL_PROMPT__ } let new_env = { $path_name: $new_path VIRTUAL_ENV: $virtual_env VIRTUAL_ENV_PROMPT: $virtual_env_prompt } + if (has-env 'TCL_LIBRARY') { + let $new_env = $new_env | insert TCL_LIBRARY __TCL_LIBRARY__ + } + if (has-env 'TK_LIBRARY') { + let $new_env = $new_env | insert TK_LIBRARY __TK_LIBRARY__ + } let old_prompt_command = if (has-env 'PROMPT_COMMAND') { $env.PROMPT_COMMAND } else { '' } let new_env = if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { $new_env diff --git a/src/virtualenv/activation/powershell/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 index bd30e2eed..9f95e4370 100644 --- a/src/virtualenv/activation/powershell/activate.ps1 +++ b/src/virtualenv/activation/powershell/activate.ps1 @@ -7,6 +7,24 @@ function global:deactivate([switch] $NonDestructive) { Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global } + if (Test-Path variable:_OLD_VIRTUAL_TCL_LIBRARY) { + $env:TCL_LIBRARY = $variable:_OLD_VIRTUAL_TCL_LIBRARY + Remove-Variable "_OLD_VIRTUAL_TCL_LIBRARY" -Scope global + } else { + if (Test-Path env:TCL_LIBRARY) { + Remove-Item env:TCL_LIBRARY -ErrorAction SilentlyContinue + } + } + + if (Test-Path variable:_OLD_VIRTUAL_TK_LIBRARY) { + $env:TK_LIBRARY = $variable:_OLD_VIRTUAL_TK_LIBRARY + Remove-Variable "_OLD_VIRTUAL_TK_LIBRARY" -Scope global + } else { + if (Test-Path env:TK_LIBRARY) { + Remove-Item env:TK_LIBRARY -ErrorAction SilentlyContinue + } + } + if (Test-Path function:_old_virtual_prompt) { $function:prompt = $function:_old_virtual_prompt Remove-Item function:\_old_virtual_prompt @@ -44,6 +62,20 @@ else { $env:VIRTUAL_ENV_PROMPT = $( Split-Path $env:VIRTUAL_ENV -Leaf ) } +if (__TCL_LIBRARY__ -ne "") { + if (Test-Path env:TCL_LIBRARY) { + New-Variable -Scope global -Name _OLD_VIRTUAL_TCL_LIBRARY -Value $env:TCL_LIBRARY + } + $env:TCL_LIBRARY = __TCL_LIBRARY__ +} + +if (__TK_LIBRARY__ -ne "") { + if (Test-Path env:TK_LIBRARY) { + New-Variable -Scope global -Name _OLD_VIRTUAL_TK_LIBRARY -Value $env:TK_LIBRARY + } + $env:TK_LIBRARY = __TK_LIBRARY__ +} + New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH $env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index 6fa4474d2..83229441a 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -47,6 +47,8 @@ def replacements(self, creator, dest_folder): # noqa: ARG002 "__VIRTUAL_NAME__": creator.env_name, "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), "__PATH_SEP__": os.pathsep, + "__TCL_LIBRARY__": creator.interpreter.tcl_lib or "", + "__TK_LIBRARY__": creator.interpreter.tk_lib or "", } def _generate(self, replacements, templates, to_folder, creator): diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 6f2575a76..ed9962034 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -123,6 +123,8 @@ def abs_path(v): self.sysconfig_vars = {i: sysconfig.get_config_var(i or "") for i in config_var_keys} + self.tcl_lib, self.tk_lib = self._get_tcl_tk_libs() if "TCL_LIBRARY" in os.environ else None, None + confs = { k: (self.system_prefix if v is not None and v.startswith(self.prefix) else v) for k, v in self.sysconfig_vars.items() @@ -132,6 +134,59 @@ def abs_path(v): self.max_size = getattr(sys, "maxsize", getattr(sys, "maxint", None)) self._creators = None + @staticmethod + def _get_tcl_tk_libs(): + """ + Detects the tcl and tk libraries using tkinter. + + This works reliably but spins up tkinter, which is heavy if you don't need it. + """ + tcl_lib, tk_lib = None, None + try: + import tkinter as tk # noqa: PLC0415 + except ImportError: + pass + else: + try: + tcl = tk.Tcl() + tcl_lib = tcl.eval("info library") + + # Try to get TK library path directly first + try: + tk_lib = tcl.eval("set tk_library") + if tk_lib and os.path.isdir(tk_lib): + pass # We found it directly + else: + tk_lib = None # Reset if invalid + except tk.TclError: + tk_lib = None + + # If direct query failed, try constructing the path + if tk_lib is None: + tk_version = tcl.eval("package require Tk") + tcl_parent = os.path.dirname(tcl_lib) + + # Try different version formats + version_variants = [ + tk_version, # Full version like "8.6.12" + ".".join(tk_version.split(".")[:2]), # Major.minor like "8.6" + tk_version.split(".")[0], # Just major like "8" + ] + + for version in version_variants: + tk_lib_path = os.path.join(tcl_parent, f"tk{version}") + if not os.path.isdir(tk_lib_path): + continue + # Validate it's actually a TK directory + if os.path.exists(os.path.join(tk_lib_path, "tk.tcl")): + tk_lib = tk_lib_path + break + + except tk.TclError: + pass + + return tcl_lib, tk_lib + def _fast_get_system_executable(self): """Try to get the system executable by just looking at properties.""" if self.real_prefix or ( # noqa: PLR1702 diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 1c945436d..53a819f96 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -130,13 +130,13 @@ def _get_test_lines(self, activate_script): ] def assert_output(self, out, raw, tmp_path): - # pre-activation + """Compare _get_test_lines() with the expected values.""" assert out[0], raw assert out[1] == "None", raw assert out[2] == "None", raw - # post-activation - expected = self._creator.exe.parent / os.path.basename(sys.executable) - assert self.norm_path(out[3]) == self.norm_path(expected), raw + # self.activate_call(activate_script) runs at this point + python_exe = self._creator.exe.parent / os.path.basename(sys.executable) + assert self.norm_path(out[3]) == self.norm_path(python_exe), raw assert self.norm_path(out[4]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw assert out[5] == self._creator.env_name # Some attempts to test the prompt output print more than 1 line. @@ -232,6 +232,7 @@ def raise_on_non_source_class(): def activation_python(request, tmp_path_factory, special_char_name, current_fastest): dest = os.path.join(str(tmp_path_factory.mktemp("activation-tester-env")), special_char_name) cmd = ["--without-pip", dest, "--creator", current_fastest, "-vv", "--no-periodic-update"] + # `params` is accessed here. https://docs.pytest.org/en/stable/reference/reference.html#pytest-fixture if request.param: cmd += ["--prompt", special_char_name] session = cli_run(cmd) diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index d89f1606a..4ebe5c063 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -1,11 +1,68 @@ from __future__ import annotations +from argparse import Namespace + import pytest from virtualenv.activation import BashActivator from virtualenv.info import IS_WIN +@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") +@pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("/path/to/tcl", "/path/to/tk", True), + (None, None, False), + ], +) +def test_bash_tkinter_generation(tmp_path, tcl_lib, tk_lib, present): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = tcl_lib + interpreter.tk_lib = tk_lib + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = BashActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate").read_text(encoding="utf-8") + + # THEN + # The teardown logic is always present in deactivate() + assert "unset _OLD_VIRTUAL_TCL_LIBRARY" in content + assert "unset _OLD_VIRTUAL_TK_LIBRARY" in content + + if present: + assert "if [ /path/to/tcl != \"''\" ]; then" in content + assert "TCL_LIBRARY=/path/to/tcl" in content + assert "export TCL_LIBRARY" in content + + assert "if [ /path/to/tk != \"''\" ]; then" in content + assert "TK_LIBRARY=/path/to/tk" in content + assert "export TK_LIBRARY" in content + else: + # When not present, the if condition is false, so the block is not executed + assert "if [ '' != \"''\" ]; then" in content + assert "TCL_LIBRARY=''" in content + # The export is inside the if, so this is fine + assert "export TCL_LIBRARY" in content + + @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") @pytest.mark.parametrize("hashing_enabled", [True, False]) def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester): diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py index 13d84442e..db2595860 100644 --- a/tests/unit/activation/test_batch.py +++ b/tests/unit/activation/test_batch.py @@ -1,10 +1,57 @@ from __future__ import annotations +from argparse import Namespace + import pytest from virtualenv.activation import BatchActivator +@pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("C:\\tcl", "C:\\tk", True), + (None, None, False), + ], +) +def test_batch_tkinter_generation(tmp_path, tcl_lib, tk_lib, present): + # GIVEN + class MockInterpreter: + os = "nt" + + interpreter = MockInterpreter() + interpreter.tcl_lib = tcl_lib + interpreter.tk_lib = tk_lib + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = BatchActivator(options) + + # WHEN + activator.generate(creator) + activate_content = (creator.bin_dir / "activate.bat").read_text(encoding="utf-8") + deactivate_content = (creator.bin_dir / "deactivate.bat").read_text(encoding="utf-8") + + # THEN + if present: + assert '@if NOT "C:\\tcl"=="" @set "TCL_LIBRARY=C:\\tcl"' in activate_content + assert '@if NOT "C:\\tk"=="" @set "TK_LIBRARY=C:\\tk"' in activate_content + assert "if defined _OLD_VIRTUAL_TCL_LIBRARY" in deactivate_content + assert "if defined _OLD_VIRTUAL_TK_LIBRARY" in deactivate_content + else: + assert '@if NOT ""=="" @set "TCL_LIBRARY="' in activate_content + assert '@if NOT ""=="" @set "TK_LIBRARY="' in activate_content + + @pytest.mark.usefixtures("activation_python") def test_batch(activation_tester_class, activation_tester, tmp_path): version_script = tmp_path / "version.bat" diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py index 125ba42d4..5cea684ec 100644 --- a/tests/unit/activation/test_csh.py +++ b/tests/unit/activation/test_csh.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from argparse import Namespace from shutil import which from subprocess import check_output @@ -10,6 +11,48 @@ from virtualenv.activation import CShellActivator +@pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("/path/to/tcl", "/path/to/tk", True), + (None, None, False), + ], +) +def test_cshell_tkinter_generation(tmp_path, tcl_lib, tk_lib, present): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = tcl_lib + interpreter.tk_lib = tk_lib + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = CShellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.csh").read_text(encoding="utf-8") + + if present: + assert "test $?_OLD_VIRTUAL_TCL_LIBRARY != 0" in content + assert "test $?_OLD_VIRTUAL_TK_LIBRARY != 0" in content + assert "setenv TCL_LIBRARY /path/to/tcl" in content + assert "setenv TK_LIBRARY /path/to/tk" in content + else: + assert "setenv TCL_LIBRARY ''" in content + + def test_csh(activation_tester_class, activation_tester): exe = f"tcsh{'.exe' if sys.platform == 'win32' else ''}" if which(exe): diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py index 0f3bee25f..c15a8f513 100644 --- a/tests/unit/activation/test_fish.py +++ b/tests/unit/activation/test_fish.py @@ -2,6 +2,7 @@ import os import sys +from argparse import Namespace import pytest @@ -9,6 +10,48 @@ from virtualenv.info import IS_WIN +@pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("/path/to/tcl", "/path/to/tk", True), + (None, None, False), + ], +) +def test_fish_tkinter_generation(tmp_path, tcl_lib, tk_lib, present): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = tcl_lib + interpreter.tk_lib = tk_lib + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = FishActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.fish").read_text(encoding="utf-8") + + # THEN + if present: + assert "set -gx TCL_LIBRARY '/path/to/tcl'" in content + assert "set -gx TK_LIBRARY '/path/to/tk'" in content + else: + assert "if test -n ''\n if set -q TCL_LIBRARY;" in content + assert "if test -n ''\n if set -q TK_LIBRARY;" in content + + @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path): monkeypatch.setenv("HOME", str(tmp_path)) @@ -46,11 +89,11 @@ def _get_test_lines(self, activate_script): ] def assert_output(self, out, raw, _): - # pre-activation + """Compare _get_test_lines() with the expected values.""" assert out[0], raw assert out[1] == "None", raw assert out[2] == "None", raw - # post-activation + # self.activate_call(activate_script) runs at this point expected = self._creator.exe.parent / os.path.basename(sys.executable) assert self.norm_path(out[4]) == self.norm_path(expected), raw assert self.norm_path(out[5]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py index fbf75e397..08c5cb1a1 100644 --- a/tests/unit/activation/test_nushell.py +++ b/tests/unit/activation/test_nushell.py @@ -1,11 +1,48 @@ from __future__ import annotations +from argparse import Namespace from shutil import which from virtualenv.activation import NushellActivator from virtualenv.info import IS_WIN +def test_nushell_tkinter_generation(tmp_path): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = "/path/to/tcl" + interpreter.tk_lib = "/path/to/tk" + quoted_tcl_path = NushellActivator.quote(interpreter.tcl_lib) + quoted_tk_path = NushellActivator.quote(interpreter.tk_lib) + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = NushellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.nu").read_text(encoding="utf-8") + + # THEN + expected_tcl = f"let $new_env = $new_env | insert TCL_LIBRARY {quoted_tcl_path}" + expected_tk = f"let $new_env = $new_env | insert TK_LIBRARY {quoted_tk_path}" + + assert expected_tcl in content + assert expected_tk in content + + def test_nushell(activation_tester_class, activation_tester): class Nushell(activation_tester_class): def __init__(self, session) -> None: diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py index dab5748d7..2a48956cf 100644 --- a/tests/unit/activation/test_powershell.py +++ b/tests/unit/activation/test_powershell.py @@ -1,12 +1,59 @@ from __future__ import annotations import sys +from argparse import Namespace import pytest from virtualenv.activation import PowerShellActivator +@pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("C:\\tcl", "C:\\tk", True), + (None, None, False), + ], +) +def test_powershell_tkinter_generation(tmp_path, tcl_lib, tk_lib, present): + # GIVEN + class MockInterpreter: + os = "nt" + + interpreter = MockInterpreter() + interpreter.tcl_lib = tcl_lib + interpreter.tk_lib = tk_lib + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = PowerShellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.ps1").read_text(encoding="utf-8-sig") + + # THEN + if present: + assert "if ('C:\\tcl' -ne \"\")" in content + assert "$env:TCL_LIBRARY = 'C:\\tcl'" in content + assert "if ('C:\\tk' -ne \"\")" in content + assert "$env:TK_LIBRARY = 'C:\\tk'" in content + assert "if (Test-Path variable:_OLD_VIRTUAL_TCL_LIBRARY)" in content + assert "if (Test-Path variable:_OLD_VIRTUAL_TK_LIBRARY)" in content + else: + assert "if ('' -ne \"\")" in content + assert "$env:TCL_LIBRARY = ''" in content + + @pytest.mark.slow def test_powershell(activation_tester_class, activation_tester, monkeypatch): monkeypatch.setenv("TERM", "xterm")