Skip to content

Commit a6a26ff

Browse files
committed
pythongh-120678: pyrepl: Include globals from modules passed with -i
1 parent a046c84 commit a6a26ff

5 files changed

Lines changed: 100 additions & 61 deletions

File tree

‎Lib/_pyrepl/__main__.py‎

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,8 @@
1-
import os
2-
import sys
1+
# Important: put as few things as possible in the global namespace of this module,
2+
# as it's easy for things in this module's `__globals__` to accidentally end up
3+
# in the globals of the REPL
34

4-
CAN_USE_PYREPL: bool
5-
if sys.platform != "win32":
6-
CAN_USE_PYREPL = True
7-
else:
8-
CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2
9-
10-
11-
def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
12-
global CAN_USE_PYREPL
13-
if not CAN_USE_PYREPL:
14-
return sys._baserepl()
15-
16-
startup_path = os.getenv("PYTHONSTARTUP")
17-
if pythonstartup and startup_path:
18-
import tokenize
19-
with tokenize.open(startup_path) as f:
20-
startup_code = compile(f.read(), startup_path, "exec")
21-
exec(startup_code)
22-
23-
# set sys.{ps1,ps2} just before invoking the interactive interpreter. This
24-
# mimics what CPython does in pythonrun.c
25-
if not hasattr(sys, "ps1"):
26-
sys.ps1 = ">>> "
27-
if not hasattr(sys, "ps2"):
28-
sys.ps2 = "... "
29-
30-
run_interactive = None
31-
try:
32-
import errno
33-
if not os.isatty(sys.stdin.fileno()):
34-
raise OSError(errno.ENOTTY, "tty required", "stdin")
35-
from .simple_interact import check
36-
if err := check():
37-
raise RuntimeError(err)
38-
from .simple_interact import run_multiline_interactive_console
39-
run_interactive = run_multiline_interactive_console
40-
except Exception as e:
41-
from .trace import trace
42-
msg = f"warning: can't use pyrepl: {e}"
43-
trace(msg)
44-
print(msg, file=sys.stderr)
45-
CAN_USE_PYREPL = False
46-
if run_interactive is None:
47-
return sys._baserepl()
48-
return run_interactive(mainmodule)
5+
from ._main import interactive_console
496

507
if __name__ == "__main__":
518
interactive_console()

‎Lib/_pyrepl/_main.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
import sys
3+
4+
CAN_USE_PYREPL: bool
5+
if sys.platform != "win32":
6+
CAN_USE_PYREPL = True
7+
else:
8+
CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2
9+
10+
11+
def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
12+
global CAN_USE_PYREPL
13+
if not CAN_USE_PYREPL:
14+
return sys._baserepl()
15+
16+
startup_path = os.getenv("PYTHONSTARTUP")
17+
if pythonstartup and startup_path:
18+
import tokenize
19+
with tokenize.open(startup_path) as f:
20+
startup_code = compile(f.read(), startup_path, "exec")
21+
exec(startup_code)
22+
23+
# set sys.{ps1,ps2} just before invoking the interactive interpreter. This
24+
# mimics what CPython does in pythonrun.c
25+
if not hasattr(sys, "ps1"):
26+
sys.ps1 = ">>> "
27+
if not hasattr(sys, "ps2"):
28+
sys.ps2 = "... "
29+
30+
run_interactive = None
31+
try:
32+
import errno
33+
if not os.isatty(sys.stdin.fileno()):
34+
raise OSError(errno.ENOTTY, "tty required", "stdin")
35+
from .simple_interact import check
36+
if err := check():
37+
raise RuntimeError(err)
38+
from .simple_interact import run_multiline_interactive_console
39+
run_interactive = run_multiline_interactive_console
40+
except Exception as e:
41+
from .trace import trace
42+
msg = f"warning: can't use pyrepl: {e}"
43+
trace(msg)
44+
print(msg, file=sys.stderr)
45+
CAN_USE_PYREPL = False
46+
if run_interactive is None:
47+
return sys._baserepl()
48+
return run_interactive(mainmodule)

‎Lib/_pyrepl/simple_interact.py‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,30 @@ def _clear_screen():
8080
"clear": _clear_screen,
8181
}
8282

83-
DEFAULT_NAMESPACE: dict[str, Any] = {
84-
'__name__': '__main__',
85-
'__doc__': None,
86-
'__package__': None,
87-
'__loader__': None,
88-
'__spec__': None,
89-
'__annotations__': {},
90-
'__builtins__': builtins,
91-
}
83+
84+
def default_namespace() -> dict[str, Any]:
85+
ns = {}
86+
for key, value in sys.modules["__main__"].__dict__.items():
87+
# avoid `getattr(value, "__module__", "")`,
88+
# as some objects raise `TypeError` on attribute access, etc.
89+
try:
90+
module = value.__module__
91+
except Exception:
92+
pass
93+
else:
94+
if module.split(".")[0] == "_pyrepl":
95+
continue
96+
ns[key] = value
97+
return ns
98+
9299

93100
def run_multiline_interactive_console(
94101
mainmodule: ModuleType | None = None,
95102
future_flags: int = 0,
96103
console: code.InteractiveConsole | None = None,
97104
) -> None:
98105
from .readline import _setup
99-
namespace = mainmodule.__dict__ if mainmodule else DEFAULT_NAMESPACE
106+
namespace = mainmodule.__dict__ if mainmodule else default_namespace()
100107
_setup(namespace)
101108

102109
if console is None:

‎Lib/test/test_pyrepl/test_pyrepl.py‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import io
22
import itertools
33
import os
4+
import pathlib
45
import rlcompleter
56
import select
67
import subprocess
78
import sys
9+
import tempfile
810
from unittest import TestCase, skipUnless
911
from unittest.mock import patch
1012
from test.support import force_not_colorized
@@ -844,15 +846,28 @@ class TestMain(TestCase):
844846
@force_not_colorized
845847
def test_exposed_globals_in_repl(self):
846848
expected_output = (
847-
"[\'__annotations__\', \'__builtins__\', \'__doc__\', \'__loader__\', "
848-
"\'__name__\', \'__package__\', \'__spec__\']"
849+
"['__annotations__', '__builtins__', '__cached__', '__doc__', "
850+
"'__file__', '__loader__', '__name__', '__package__', '__spec__']"
849851
)
850852
output, exit_code = self.run_repl(["sorted(dir())", "exit"])
851853
if "can\'t use pyrepl" in output:
852854
self.skipTest("pyrepl not available")
853855
self.assertEqual(exit_code, 0)
854856
self.assertIn(expected_output, output)
855857

858+
@force_not_colorized
859+
def test_globals_from_file_passed_included_in_repl_globals(self):
860+
with tempfile.TemporaryDirectory() as td:
861+
fake_main = pathlib.Path(td, "foo.py")
862+
fake_main.write_text("FOO = 42", encoding="utf-8")
863+
output, exit_code = self.run_repl(
864+
["FOO", "exit"], main_module=str(fake_main)
865+
)
866+
if "can\'t use pyrepl" in output:
867+
self.skipTest("pyrepl not available")
868+
self.assertEqual(exit_code, 0)
869+
self.assertIn("42", output)
870+
856871
def test_dumb_terminal_exits_cleanly(self):
857872
env = os.environ.copy()
858873
env.update({"TERM": "dumb"})
@@ -862,10 +877,19 @@ def test_dumb_terminal_exits_cleanly(self):
862877
self.assertNotIn("Exception", output)
863878
self.assertNotIn("Traceback", output)
864879

865-
def run_repl(self, repl_input: str | list[str], env: dict | None = None) -> tuple[str, int]:
880+
def run_repl(
881+
self,
882+
repl_input: str | list[str],
883+
env: dict | None = None,
884+
*,
885+
main_module: str | None = None
886+
) -> tuple[str, int]:
866887
master_fd, slave_fd = pty.openpty()
888+
repl_args = [sys.executable, "-u", "-i"]
889+
if main_module is not None:
890+
repl_args.append(main_module)
867891
process = subprocess.Popen(
868-
[sys.executable, "-i", "-u"],
892+
repl_args,
869893
stdin=slave_fd,
870894
stdout=slave_fd,
871895
stderr=slave_fd,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix regression in the new REPL that meant that globals from files passed
2+
using the `-i` argument would not be included in the REPL's global
3+
namespace. Patch by Alex Waygood.

0 commit comments

Comments
 (0)