]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-127960 Fix the REPL to set the correct namespace by setting the correct `__main__...
authorYuichiro Tachibana (Tsuchiya) <t.yic.yt@gmail.com>
Thu, 22 May 2025 00:18:00 +0000 (19:18 -0500)
committerGitHub <noreply@github.com>
Thu, 22 May 2025 00:18:00 +0000 (02:18 +0200)
The `__main__` module imported in the `_pyrepl` module points to the `_pyrepl` module itself when the interpreter was launched without `-m` option and didn't execute a module,
while it's an unexpected behavior that `__main__` can be `_pyrepl` and relative imports such as `from . import *` works based on the `_pyrepl` module.

Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/_module_completer.py
Lib/_pyrepl/main.py
Lib/_pyrepl/readline.py
Lib/test/support/__init__.py
Lib/test/test_pyrepl/test_pyrepl.py
Misc/NEWS.d/next/Core_and_Builtins/2025-05-21-18-02-56.gh-issue-127960.W3J_2X.rst [new file with mode: 0644]
Modules/main.c

index 9aafb55090e2ce1c3e49078b6da296c8b904bbbf..494a501101a9b296dc8077480cbd9c91f6a5ea53 100644 (file)
@@ -17,8 +17,8 @@ if TYPE_CHECKING:
 
 
 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:
index a6f824dcc4ad14043a0fb4b10cd57e6c83c01cdc..447eb1e551e7743c42efbfffead65f3d47a59252 100644 (file)
@@ -1,6 +1,7 @@
 import errno
 import os
 import sys
+import types
 
 
 CAN_USE_PYREPL: bool
@@ -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")
index 560a9db192169e1c39b5bf9442ddf41cdd18402a..572eee520e53f3ba27238bb6d189f6c1e050eeb1 100644 (file)
@@ -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
index 9b6e80fdad974700c2cafc714e8112bcc3e6470e..b7cd7940eb15b34d932467b510604268116f51d5 100644 (file)
@@ -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",
index 29762232d43b890a8943f74c4b7eb9d7d47de8ae..abb4bd1bc25fb11b381a829541009087a328b73d 100644 (file)
@@ -926,6 +926,7 @@ class TestPyReplModuleCompleter(TestCase):
     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
@@ -1022,13 +1023,15 @@ class TestPyReplModuleCompleter(TestCase):
 
     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)
 
@@ -1397,7 +1400,7 @@ class TestMain(ReplTestCase):
             )
 
     @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
 
@@ -1406,9 +1409,13 @@ class TestMain(ReplTestCase):
             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:
@@ -1427,7 +1434,13 @@ class TestMain(ReplTestCase):
                     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():
@@ -1440,6 +1453,23 @@ class TestMain(ReplTestCase):
         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",
@@ -1449,6 +1479,16 @@ class TestMain(ReplTestCase):
         }
         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",
@@ -1458,26 +1498,32 @@ class TestMain(ReplTestCase):
         }
         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)
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-21-18-02-56.gh-issue-127960.W3J_2X.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-21-18-02-56.gh-issue-127960.W3J_2X.rst
new file mode 100644 (file)
index 0000000..730d8a5
--- /dev/null
@@ -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.
index 2be194bdadf7d03fd23f05ad0b78b58f6b33ff92..2d7ed25f5f979005ccea55c7d39ae01ad3e5ca79 100644 (file)
@@ -269,13 +269,14 @@ error:
 
 
 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) {
@@ -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();
@@ -311,6 +318,7 @@ done:
     Py_XDECREF(empty_tuple);
     Py_XDECREF(console);
     Py_XDECREF(pyrepl);
+    Py_XDECREF(main_module);
     return res;
 }
 
@@ -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);
 }
 
 
@@ -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;
 }