From 74b55f43fee8a47b0eebc77bc754efb204428513 Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Sun, 19 Jul 2026 13:44:14 +0530 Subject: [PATCH 1/2] Fix #26039: REPL prompt disappears when builtins/globals shadowed under PYTHONSTARTUP --- python_files/pythonrc.py | 40 +++++++++++++++----- python_files/tests/test_shell_integration.py | 18 +++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index 3042ffb7a309..e248ebe49421 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -7,6 +7,21 @@ original_ps1 = ">>> " is_wsl = "microsoft-standard-WSL" in platform.release() +# NOTE: PYTHONSTARTUP executes this file's code directly inside the user's +# __main__ namespace, so everything defined below actually lives in +# __main__.__dict__. That means PS1.__str__.__globals__ IS the user's +# namespace: if the user later shadows a name we rely on at prompt-render +# time (e.g. `int = 20`, `sys = 1`, `original_ps1 = ...`), a plain global +# lookup would resolve to the user's value instead of ours and raise, +# silently killing the prompt (see #26039). Capture the real objects here, +# before any user code runs, so later reassignment in __main__ can't affect +# us. +_int = int +_str = str +_bool = bool +_sys = sys +_original_ps1 = original_ps1 + class REPLHooks: def __init__(self): @@ -33,12 +48,18 @@ def my_excepthook(self, type_, value, traceback): def get_last_command(): # Get the last history item last_command = "" - if sys.platform != "win32": - last_command = readline.get_history_item(readline.get_current_history_length()) + if _sys.platform != "win32": + last_command = _readline.get_history_item(_readline.get_current_history_length()) return last_command +# Capture the function object itself, not just the name: `get_last_command` +# is also just a name in __main__, so it could be shadowed the same way. +_get_last_command = get_last_command +_readline = readline if sys.platform != "win32" else None + + class PS1: hooks = REPLHooks() sys.excepthook = hooks.my_excepthook @@ -46,27 +67,27 @@ 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 = "" # For non-windows allow recent_command history. - if sys.platform != "win32": + if _sys.platform != "win32": result = "{soh}{command_executed}{command_line}{command_finished}{prompt_started}{stx}{prompt}{soh}{command_start}{stx}".format( soh="\001", stx="\002", command_executed="\x1b]633;C\x07", - command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", - command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + command_line="\x1b]633;E;" + _str(_get_last_command()) + "\x07", + command_finished="\x1b]633;D;" + _str(exit_code) + "\x07", prompt_started="\x1b]633;A\x07", - prompt=original_ps1, + prompt=_original_ps1, command_start="\x1b]633;B\x07", ) else: result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( - command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + command_finished="\x1b]633;D;" + _str(exit_code) + "\x07", prompt_started="\x1b]633;A\x07", - prompt=original_ps1, + prompt=_original_ps1, command_start="\x1b]633;B\x07", command_executed="\x1b]633;C\x07", ) @@ -86,3 +107,4 @@ def __repr__(self): print("Cmd click to launch VS Code Native REPL") else: print("Ctrl click to launch VS Code Native REPL") + \ No newline at end of file diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 7503a725b6d1..5a8b0c9bd094 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -1,11 +1,29 @@ import importlib import platform import sys +from pathlib import Path from unittest.mock import Mock import pythonrc is_wsl = "microsoft-standard-WSL" in platform.release() + +PYTHONRC_PATH = Path(pythonrc.__file__) + + +def test_decoration_success(): + importlib.reload(pythonrc) + ps1 = pythonrc.PS1() + + ps1.hooks.failure_flag = False + result = str(ps1) + if sys.platform != "win32" and (not is_wsl): + 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" + ) + else: + pass def test_decoration_success(): From e9785f4edb5bef5e6214c78ceec65b8476511cdd Mon Sep 17 00:00:00 2001 From: Mohit Yadav Date: Sun, 19 Jul 2026 13:48:49 +0530 Subject: [PATCH 2/2] Fix duplicate test and restore missing regression test --- python_files/tests/test_shell_integration.py | 53 ++++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 5a8b0c9bd094..43250f96a7b3 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -2,28 +2,14 @@ import platform import sys from pathlib import Path +from typing import Any from unittest.mock import Mock import pythonrc is_wsl = "microsoft-standard-WSL" in platform.release() - + PYTHONRC_PATH = Path(pythonrc.__file__) - - -def test_decoration_success(): - importlib.reload(pythonrc) - ps1 = pythonrc.PS1() - - ps1.hooks.failure_flag = False - result = str(ps1) - if sys.platform != "win32" and (not is_wsl): - 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" - ) - else: - pass def test_decoration_success(): @@ -99,3 +85,38 @@ def test_print_statement_non_darwin(monkeypatch): m.setattr("builtins.print", Mock()) importlib.reload(sys.modules["pythonrc"]) print.assert_any_call("Ctrl click to launch VS Code Native REPL") + + +def test_prompt_survives_shadowed_builtins_under_pythonstartup(): + """Regression test for #26039. + + PYTHONSTARTUP executes pythonrc.py's code directly inside __main__, not + as a normal module import, so PS1.__str__.__globals__ ends up being the + user's own namespace. `import pythonrc` (used by every other test in + this file) does not reproduce that, since it gives PS1 its own module + namespace instead. Simulate the real PYTHONSTARTUP path here: exec the + source into a stand-in __main__ dict, then shadow the names the prompt + depends on exactly as a user would (`int = 20`, `sys = 1`, etc.) and + confirm str(sys.ps1) still renders instead of raising. + """ + if sys.platform == "win32" or is_wsl: + return + + main_ns: dict[str, Any] = {"__name__": "__main__"} + source = PYTHONRC_PATH.read_text() + exec(compile(source, str(PYTHONRC_PATH), "exec"), main_ns) + + ps1 = main_ns["sys"].ps1 + assert str(ps1) # sanity: works before any shadowing + + for name, value in { + "int": 20, + "sys": 1, + "str": "nope", + "bool": "nope", + "original_ps1": "hijacked", + "get_last_command": "hijacked", + }.items(): + main_ns[name] = value + assert str(ps1), f"prompt failed to render after shadowing {name!r}" + \ No newline at end of file