Skip to content

gh-127960 Fix the REPL to set the correct namespace by setting the correct __main__ module #134275

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

Merged
merged 10 commits into from
May 22, 2025
Merged
4 changes: 2 additions & 2 deletions Lib/_pyrepl/_module_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@


def make_default_module_completer() -> ModuleCompleter:
# Inside pyrepl, __package__ is set to '_pyrepl'
return ModuleCompleter(namespace={'__package__': '_pyrepl'})
# Inside pyrepl, __package__ is set to None by default
return ModuleCompleter(namespace={'__package__': None})


class ModuleCompleter:
Expand Down
11 changes: 5 additions & 6 deletions Lib/_pyrepl/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import errno
import os
import sys
import types


CAN_USE_PYREPL: bool
Expand Down Expand Up @@ -29,12 +30,10 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
print(FAIL_REASON, file=sys.stderr)
return sys._baserepl()

if mainmodule:
namespace = mainmodule.__dict__
else:
import __main__
namespace = __main__.__dict__
namespace.pop("__pyrepl_interactive_console", None)
if not mainmodule:
mainmodule = types.ModuleType("__main__")

namespace = mainmodule.__dict__

# sys._baserepl() above does this internally, we do it here
startup_path = os.getenv("PYTHONSTARTUP")
Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ def _setup(namespace: Mapping[str, Any]) -> None:
# set up namespace in rlcompleter, which requires it to be a bona fide dict
if not isinstance(namespace, dict):
namespace = dict(namespace)
_wrapper.config.module_completer = ModuleCompleter(namespace)
_wrapper.config.readline_completer = RLCompleter(namespace).complete

# this is not really what readline.c does. Better than nothing I guess
Expand Down
6 changes: 0 additions & 6 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2929,12 +2929,6 @@ def make_clean_env() -> dict[str, str]:
return clean_env


def initialized_with_pyrepl():
"""Detect whether PyREPL was used during Python initialization."""
# If the main module has a __file__ attribute it's a Python module, which means PyREPL.
return hasattr(sys.modules["__main__"], "__file__")


WINDOWS_STATUS = {
0xC0000005: "STATUS_ACCESS_VIOLATION",
0xC00000FD: "STATUS_STACK_OVERFLOW",
Expand Down
78 changes: 62 additions & 16 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ def tearDown(self):
def prepare_reader(self, events, namespace):
console = FakeConsole(events)
config = ReadlineConfig()
config.module_completer = ModuleCompleter(namespace)
config.readline_completer = rlcompleter.Completer(namespace).complete
reader = ReadlineAlikeReader(console=console, config=config)
return reader
Expand Down Expand Up @@ -961,13 +962,15 @@ def test_import_completions(self):

def test_relative_import_completions(self):
cases = (
("from .readl\t\n", "from .readline"),
("from . import readl\t\n", "from . import readline"),
(None, "from .readl\t\n", "from .readl"),
(None, "from . import readl\t\n", "from . import readl"),
("_pyrepl", "from .readl\t\n", "from .readline"),
("_pyrepl", "from . import readl\t\n", "from . import readline"),
)
for code, expected in cases:
for package, code, expected in cases:
with self.subTest(code=code):
events = code_to_events(code)
reader = self.prepare_reader(events, namespace={})
reader = self.prepare_reader(events, namespace={"__package__": package})
output = reader.readline()
self.assertEqual(output, expected)

Expand Down Expand Up @@ -1336,7 +1339,7 @@ def _assertMatchOK(
)

@force_not_colorized
def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False):
def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False, pythonstartup=False):
clean_env = make_clean_env()
clean_env["NO_COLOR"] = "1" # force_not_colorized doesn't touch subprocesses

Expand All @@ -1345,9 +1348,13 @@ def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False
blue.mkdir()
mod = blue / "calx.py"
mod.write_text("FOO = 42", encoding="utf-8")
startup = blue / "startup.py"
startup.write_text("BAR = 64", encoding="utf-8")
commands = [
"print(f'^{" + var + "=}')" for var in expectations
] + ["exit()"]
if pythonstartup:
clean_env["PYTHONSTARTUP"] = str(startup)
if as_file and as_module:
self.fail("as_file and as_module are mutually exclusive")
elif as_file:
Expand All @@ -1366,7 +1373,13 @@ def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False
skip=True,
)
else:
self.fail("Choose one of as_file or as_module")
output, exit_code = self.run_repl(
commands,
cmdline_args=[],
env=clean_env,
cwd=td,
skip=True,
)

self.assertEqual(exit_code, 0)
for var, expected in expectations.items():
Expand All @@ -1379,6 +1392,23 @@ def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)

def test_globals_initialized_as_default(self):
expectations = {
"__name__": "'__main__'",
"__package__": "None",
# "__file__" is missing in -i, like in the basic REPL
}
self._run_repl_globals_test(expectations)

def test_globals_initialized_from_pythonstartup(self):
expectations = {
"BAR": "64",
"__name__": "'__main__'",
"__package__": "None",
# "__file__" is missing in -i, like in the basic REPL
}
self._run_repl_globals_test(expectations, pythonstartup=True)

def test_inspect_keeps_globals_from_inspected_file(self):
expectations = {
"FOO": "42",
Expand All @@ -1388,6 +1418,16 @@ def test_inspect_keeps_globals_from_inspected_file(self):
}
self._run_repl_globals_test(expectations, as_file=True)

def test_inspect_keeps_globals_from_inspected_file_with_pythonstartup(self):
expectations = {
"FOO": "42",
"BAR": "64",
"__name__": "'__main__'",
"__package__": "None",
# "__file__" is missing in -i, like in the basic REPL
}
self._run_repl_globals_test(expectations, as_file=True, pythonstartup=True)

def test_inspect_keeps_globals_from_inspected_module(self):
expectations = {
"FOO": "42",
Expand All @@ -1397,26 +1437,32 @@ def test_inspect_keeps_globals_from_inspected_module(self):
}
self._run_repl_globals_test(expectations, as_module=True)

def test_inspect_keeps_globals_from_inspected_module_with_pythonstartup(self):
expectations = {
"FOO": "42",
"BAR": "64",
"__name__": "'__main__'",
"__package__": "'blue'",
"__file__": re.compile(r"^'.*calx.py'$"),
}
self._run_repl_globals_test(expectations, as_module=True, pythonstartup=True)

@force_not_colorized
def test_python_basic_repl(self):
env = os.environ.copy()
commands = ("from test.support import initialized_with_pyrepl\n"
"initialized_with_pyrepl()\n"
"exit()\n")

pyrepl_commands = "clear\nexit()\n"
env.pop("PYTHON_BASIC_REPL", None)
output, exit_code = self.run_repl(commands, env=env, skip=True)
output, exit_code = self.run_repl(pyrepl_commands, env=env, skip=True)
self.assertEqual(exit_code, 0)
self.assertIn("True", output)
self.assertNotIn("False", output)
self.assertNotIn("Exception", output)
self.assertNotIn("NameError", output)
self.assertNotIn("Traceback", output)

basic_commands = "help\nexit()\n"
env["PYTHON_BASIC_REPL"] = "1"
output, exit_code = self.run_repl(commands, env=env)
output, exit_code = self.run_repl(basic_commands, env=env)
self.assertEqual(exit_code, 0)
self.assertIn("False", output)
self.assertNotIn("True", output)
self.assertIn("Type help() for interactive help", output)
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PyREPL interactive shell no longer starts with ``__package__`` and
``__file__`` global names set to ``_pyrepl`` package internals. Contributed
by Yuichiro Tachibana.
16 changes: 12 additions & 4 deletions Modules/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,14 @@ pymain_run_command(wchar_t *command)


static int
pymain_start_pyrepl_no_main(void)
pymain_start_pyrepl(int pythonstartup)
{
int res = 0;
PyObject *console = NULL;
PyObject *empty_tuple = NULL;
PyObject *kwargs = NULL;
PyObject *console_result = NULL;
PyObject *main_module = NULL;

PyObject *pyrepl = PyImport_ImportModule("_pyrepl.main");
if (pyrepl == NULL) {
Expand All @@ -299,7 +300,13 @@ pymain_start_pyrepl_no_main(void)
res = pymain_exit_err_print();
goto done;
}
if (!PyDict_SetItemString(kwargs, "pythonstartup", _PyLong_GetOne())) {
main_module = PyImport_AddModuleRef("__main__");
if (main_module == NULL) {
res = pymain_exit_err_print();
goto done;
}
if (!PyDict_SetItemString(kwargs, "mainmodule", main_module)
&& !PyDict_SetItemString(kwargs, "pythonstartup", pythonstartup ? Py_True : Py_False)) {
console_result = PyObject_Call(console, empty_tuple, kwargs);
if (console_result == NULL) {
res = pymain_exit_err_print();
Expand All @@ -311,6 +318,7 @@ pymain_start_pyrepl_no_main(void)
Py_XDECREF(empty_tuple);
Py_XDECREF(console);
Py_XDECREF(pyrepl);
Py_XDECREF(main_module);
return res;
}

Expand Down Expand Up @@ -562,7 +570,7 @@ pymain_run_stdin(PyConfig *config)
int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, &cf);
return (run != 0);
}
return pymain_run_module(L"_pyrepl", 0);
return pymain_start_pyrepl(0);
}


Expand Down Expand Up @@ -595,7 +603,7 @@ pymain_repl(PyConfig *config, int *exitcode)
*exitcode = (run != 0);
return;
}
int run = pymain_start_pyrepl_no_main();
int run = pymain_start_pyrepl(1);
*exitcode = (run != 0);
return;
}
Expand Down
Loading