From f3b5e40d4125bbdf715af1c2a0b2dddbcb8f50cb Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Mon, 14 Sep 2026 15:00:37 +0530 Subject: [PATCH 1/4] Fix #26045: capture PS1 dependencies as closures to survive shadowed builtins/globals under PYTHONSTARTUP --- python_files/tests/test_shell_integration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 9902b72f3408..e10328cffa6b 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -8,6 +8,8 @@ is_wsl = "microsoft-standard-WSL" in platform.release() +PYTHONRC_PATH = Path(pythonrc.__file__) + class _Hooks(Protocol): failure_flag: bool From d562064d06e864696119bfed394c96876ed08bd1 Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Mon, 14 Sep 2026 15:19:16 +0530 Subject: [PATCH 2/4] Fix #26045: capture PS1 builtins as _initialize locals to survive shadowing under PYTHONSTARTUP --- python_files/pythonrc.py | 22 +++++++++++--- python_files/tests/test_shell_integration.py | 32 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index e06da5b664ae..817add379162 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -9,6 +9,20 @@ def _initialize(): original_ps1 = ">>> " is_wsl = "microsoft-standard-WSL" in platform.release() + # PYTHONSTARTUP executes this file's code inside the user's __main__ + # namespace, so PS1.__str__'s globals are the user's globals. If the + # user later shadows a name we rely on at prompt-render time (e.g. + # `int = 20`, `sys = 1`), a plain lookup would resolve to the user's + # value instead of ours and raise, silently killing the prompt. + # + # Capturing these as locals of _initialize (rather than as names left + # sitting in __main__) means PS1's methods reach them through closure + # cells, not global lookup - so there's no alias name in __main__ for + # user code to reassign and break in the first place. + _int = int + _bool = bool + _str = str + class ShellIntegrationSequence(str, Enum): SOH = "\001" STX = "\002" @@ -53,7 +67,7 @@ class PS1: # str will get called for every prompt with exit code to show success/failure def __str__(self): - exit_code = int(bool(self.hooks.failure_flag)) + exit_code = _int(_bool(self.hooks.failure_flag)) self.hooks.failure_flag = False # Guide following official VS Code doc for shell integration sequence: result = "" @@ -64,10 +78,10 @@ def __str__(self): stx=ShellIntegrationSequence.STX, command_executed=ShellIntegrationSequence.COMMAND_EXECUTED, command_line=ShellIntegrationSequence.COMMAND_LINE - + str(get_last_command()) + + _str(get_last_command()) + ShellIntegrationSequence.TERMINATOR, command_finished=ShellIntegrationSequence.COMMAND_FINISHED - + str(exit_code) + + _str(exit_code) + ShellIntegrationSequence.TERMINATOR, prompt_started=ShellIntegrationSequence.PROMPT_STARTED, prompt=original_ps1, @@ -76,7 +90,7 @@ def __str__(self): else: result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( command_finished=ShellIntegrationSequence.COMMAND_FINISHED - + str(exit_code) + + _str(exit_code) + ShellIntegrationSequence.TERMINATOR, prompt_started=ShellIntegrationSequence.PROMPT_STARTED, prompt=original_ps1, diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index e10328cffa6b..e53c68218a16 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -1,6 +1,7 @@ import importlib import platform import sys +from pathlib import Path from typing import Protocol, cast from unittest.mock import Mock @@ -72,6 +73,37 @@ def test_does_not_pollute_namespace(): assert not [name for name in vars(pythonrc) if not name.startswith("__")] +def test_prompt_survives_shadowed_builtins_under_pythonstartup(): + # PYTHONSTARTUP executes pythonrc's source directly inside the real + # REPL's __main__ namespace, not as an imported module. The tests + # above import pythonrc normally, which gives PS1 its own module + # namespace instead of __main__ and would never catch this. Simulate + # the real PYTHONSTARTUP path by exec-ing the source into a synthetic + # __main__-like namespace, then shadow the names PS1 relies on at + # prompt-render time and confirm rendering the prompt still works. + if sys.platform == "win32" or is_wsl: + return + + source = PYTHONRC_PATH.read_text(encoding="utf-8") + namespace = {"__name__": "__main__"} + exec(compile(source, str(PYTHONRC_PATH), "exec"), namespace) + + namespace.update( + { + "int": 20, + "bool": 20, + "str": 20, + "sys": 1, + "original_ps1": "shadowed", + "get_last_command": "shadowed", + } + ) + + ps1 = cast("_PS1", sys.ps1) + result = str(ps1) + assert result.startswith("\x01") + + if sys.platform == "darwin": def test_print_statement_darwin(monkeypatch): From 68e5cbf4159758c9278a3aa8bd39dfec4e204efc Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Mon, 14 Sep 2026 23:06:26 +0530 Subject: [PATCH 3/4] Update python_files/tests/test_shell_integration.py Co-authored-by: Eduardo Villalpando Mello --- python_files/tests/test_shell_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index e53c68218a16..9b8d3a137976 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -85,7 +85,7 @@ def test_prompt_survives_shadowed_builtins_under_pythonstartup(): return source = PYTHONRC_PATH.read_text(encoding="utf-8") - namespace = {"__name__": "__main__"} + namespace: dict[str, Any] = {"__name__": "__main__"} exec(compile(source, str(PYTHONRC_PATH), "exec"), namespace) namespace.update( From de184540f92362306b0c2fcb18f4b4f0fe83fd84 Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Mon, 14 Sep 2026 23:39:38 +0530 Subject: [PATCH 4/4] Fix #26045: use exact assertion per tests-no-partial-asserts policy; import Any for namespace annotation --- python_files/tests/test_shell_integration.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 9b8d3a137976..ac7fb3d2118d 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -2,7 +2,7 @@ import platform import sys from pathlib import Path -from typing import Protocol, cast +from typing import Any, Protocol, cast from unittest.mock import Mock import pythonrc @@ -101,7 +101,10 @@ def test_prompt_survives_shadowed_builtins_under_pythonstartup(): ps1 = cast("_PS1", sys.ps1) result = str(ps1) - assert result.startswith("\x01") + assert ( + result + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" + ) if sys.platform == "darwin":