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/425.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for Tcl and Tkinter. You're welcome.
Contributed by :user:`esafak`.
27 changes: 27 additions & 0 deletions src/virtualenv/activation/bash/activate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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-}"
Expand Down
6 changes: 6 additions & 0 deletions src/virtualenv/activation/batch/activate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
8 changes: 8 additions & 0 deletions src/virtualenv/activation/batch/deactivate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
14 changes: 13 additions & 1 deletion src/virtualenv/activation/cshell/activate.csh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__
Expand Down
8 changes: 8 additions & 0 deletions src/virtualenv/activation/fish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions src/virtualenv/activation/fish/activate.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__
Expand Down
4 changes: 4 additions & 0 deletions src/virtualenv/activation/nushell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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 "",
}


Expand Down
6 changes: 6 additions & 0 deletions src/virtualenv/activation/nushell/activate.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions src/virtualenv/activation/powershell/activate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/virtualenv/activation/via_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
55 changes: 55 additions & 0 deletions src/virtualenv/discovery/py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/activation/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading