Skip to content
Open
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
40 changes: 40 additions & 0 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,46 @@ def test_special_chars_csh(self):
self.assertTrue(env_name.encode() in lines[0])
self.assertEndsWith(lines[1], env_name.encode())

# gh-140006: the fish prompt override must keep working when a user
# function shadows a builtin it relies on.
@unittest.skipIf(os.name == 'nt', 'fish is not available on Windows')
def test_fish_activate_shadowed_builtins(self):
"""
The fish prompt override restores the exit status through `source` and
prints through `printf`/`echo`/`set_color`. A user function that
shadows one of those builtins (a common pattern for `.`-style directory
navigators) must not hijack the prompt or break status restoration.
"""
fish = shutil.which('fish')
if fish is None:
self.skipTest('fish required for this test')
rmtree(self.env_dir)
builder = venv.EnvBuilder(clear=True)
builder.create(self.env_dir)
activate = os.path.join(self.env_dir, self.bindir, 'activate.fish')
test_script = os.path.join(self.env_dir, 'test_shadowed_builtins.fish')
with open(test_script, "w") as f:
f.write(
# The pre-existing prompt reports the status it receives;
# activation copies it to _old_fish_prompt.
'function fish_prompt; builtin echo "OLDSTATUS=$status"; end\n'
f'source {shlex.quote(activate)}\n'
# Shadow every builtin the override uses. A dot-navigator that
# lists the directory is the reported failure.
'function .; builtin echo DOT_LEAK; end\n'
'function source; builtin echo SOURCE_LEAK; end\n'
'function echo; command echo ECHO_LEAK; end\n'
'function printf; command printf PRINTF_LEAK; end\n'
'function set_color; command true; end\n'
'function _exit7; return 7; end\n'
'_exit7\n'
'fish_prompt\n'
)
out, err = check_output([fish, '--no-config', test_script])
text = out.decode()
self.assertNotIn('LEAK', text)
self.assertIn('OLDSTATUS=7', text)

# gh-124651: test quoted strings on Windows
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
def test_special_chars_windows(self):
Expand Down
19 changes: 11 additions & 8 deletions Lib/venv/scripts/common/activate.fish
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ function deactivate -d "Exit virtual environment and return to normal shell env
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
if builtin functions -q _old_fish_prompt
builtin functions -e fish_prompt
builtin functions -c _old_fish_prompt fish_prompt
builtin functions -e _old_fish_prompt
end
end

set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
builtin functions -e deactivate
end
end

Expand All @@ -49,18 +49,21 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.

# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
builtin functions -c fish_prompt _old_fish_prompt

# With the original prompt function renamed, we can override with our own.
# Call every builtin through `builtin` so a user function that shadows
# `printf`, `set_color`, `echo`, or `source`/`.` cannot hijack the prompt
# (Issue #140006).
function fish_prompt
# Save the return status of the last command.
set -l old_status $status

# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s(%s)%s " (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
builtin printf "%s(%s)%s " (builtin set_color 4B8BBE) __VENV_PROMPT__ (builtin set_color normal)

# Restore the return status of the previous command.
echo "exit $old_status" | .
builtin echo "exit $old_status" | builtin source -
# Output the original/"old" prompt.
_old_fish_prompt
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The :mod:`venv` ``activate.fish`` script now calls fish builtins through
``builtin`` so a user function that shadows ``.``/``source``, ``echo``,
``printf``, ``set_color``, or ``functions`` can no longer hijack the virtual
environment prompt or break exit-status reporting.
Loading