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: 31 additions & 9 deletions python_files/pythonrc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -33,40 +48,46 @@ 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
sys.displayhook = hooks.my_displayhook

# 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",
)
Expand All @@ -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")

39 changes: 39 additions & 0 deletions python_files/tests/test_shell_integration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import importlib
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)
Expand Down Expand Up @@ -81,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}"