diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 1ff5d7cf0c51dd8..afe13366c069051 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -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): diff --git a/Lib/venv/scripts/common/activate.fish b/Lib/venv/scripts/common/activate.fish index 284a7469c99b576..867aa161a8a3ffb 100644 --- a/Lib/venv/scripts/common/activate.fish +++ b/Lib/venv/scripts/common/activate.fish @@ -15,10 +15,10 @@ 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 @@ -26,7 +26,7 @@ function deactivate -d "Exit virtual environment and return to normal shell env set -e VIRTUAL_ENV_PROMPT if test "$argv[1]" != "nondestructive" # Self-destruct! - functions -e deactivate + builtin functions -e deactivate end end @@ -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 diff --git a/Misc/NEWS.d/next/Library/2026-06-04-19-24-13.gh-issue-140006.TD8HKl.rst b/Misc/NEWS.d/next/Library/2026-06-04-19-24-13.gh-issue-140006.TD8HKl.rst new file mode 100644 index 000000000000000..6d8d66b29b19902 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-04-19-24-13.gh-issue-140006.TD8HKl.rst @@ -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.