diff --git a/Lib/_pyrepl/main.py b/Lib/_pyrepl/main.py index a6f824dcc4ad14..17c55eb9c9f7c4 100644 --- a/Lib/_pyrepl/main.py +++ b/Lib/_pyrepl/main.py @@ -35,6 +35,7 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): import __main__ namespace = __main__.__dict__ namespace.pop("__pyrepl_interactive_console", None) + namespace.pop("__file__", None) # sys._baserepl() above does this internally, we do it here startup_path = os.getenv("PYTHONSTARTUP") diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 7c1ef42a4970d7..142b8938eae3ad 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -62,6 +62,7 @@ "force_not_colorized", "BrokenIter", "in_systemd_nspawn_sync_suppressed", + "initialized_with_pyrepl", ] @@ -2849,8 +2850,8 @@ def wrapper(*args, **kwargs): 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__") + spec = getattr(sys.modules.get("__main__", None), "__spec__", None) + return getattr(spec, "name", None) == "_pyrepl.__main__" WINDOWS_STATUS = { diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index f29a7ffbd7cafd..aa9e2e68cfab80 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1087,19 +1087,13 @@ def test_exposed_globals_in_repl(self): self.skipTest("pyrepl not available") self.assertEqual(exit_code, 0) - # if `__main__` is not a file (impossible with pyrepl) + # if `__main__` is an uncached .py file (no .pyc), `__file__` was removed case1 = f"{pre}, '__doc__', {post}" in output - # if `__main__` is an uncached .py file (no .pyc) - case2 = f"{pre}, '__doc__', '__file__', {post}" in output + # if `__main__` is a cached .pyc, `__file__` was removed + case2 = f"{pre}, '__cached__', '__doc__', {post}" in output - # if `__main__` is a cached .pyc file and the .py source exists - case3 = f"{pre}, '__cached__', '__doc__', '__file__', {post}" in output - - # if `__main__` is a cached .pyc file but there's no .py source file - case4 = f"{pre}, '__cached__', '__doc__', {post}" in output - - self.assertTrue(case1 or case2 or case3 or case4, output) + self.assertTrue(case1 or case2, output) def _assertMatchOK( self, var: str, expected: str | re.Pattern, actual: str @@ -1175,7 +1169,6 @@ def test_inspect_keeps_globals_from_inspected_module(self): "FOO": "42", "__name__": "'__main__'", "__package__": "'blue'", - "__file__": re.compile(r"^'.*calx.py'$"), } self._run_repl_globals_test(expectations, as_module=True) @@ -1318,6 +1311,12 @@ def test_null_byte(self): self.assertEqual(exit_code, 0) self.assertNotIn("TypeError", output) + def test_inspect_getsource(self): + code = "import inspect\nclass A: pass\n\nprint(inspect.getsource(A))\nexit()\n" + output, exit_code = self.run_repl(code) + self.assertEqual(exit_code, 0) + self.assertIn("OSError('source code not available')", output) + def test_readline_history_file(self): # skip, if readline module is not available readline = import_module('readline') diff --git a/Misc/NEWS.d/next/Library/2024-10-27-11-46-13.gh-issue-126019.I_GHhZ.rst b/Misc/NEWS.d/next/Library/2024-10-27-11-46-13.gh-issue-126019.I_GHhZ.rst new file mode 100644 index 00000000000000..92d31b2b08abc4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-27-11-46-13.gh-issue-126019.I_GHhZ.rst @@ -0,0 +1,3 @@ +Remove the ``__file__`` attribute from the ``__main__`` namespace created by +PyREPL, to avoid :func:`inspect.getsource` from fetching unrelated source +code for classes defined in the REPL.