Skip to content
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