-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix #26039: REPL prompt disappears when builtins/globals shadowed under PYTHONSTARTUP #26045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
74b55f4
e9785f4
5226ae1
d063f0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The private aliases remain mutable entries in the user's [verified] |
||
| 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", | ||
| ) | ||
|
|
||
| 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) | ||
|
|
@@ -81,3 +85,37 @@ 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(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Remove the issue reference from this test docstring and keep the linkage in PR or commit metadata. Committed regression tests are explicitly covered by [verified] |
||
| """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}" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the issue reference from this production-code comment.
docs/pylancewiki/review/issue-references.mdprohibits issue links in production-code comments, with no applicable exception here.[verified]