Skip to content

Commit

Permalink
pythongh-120678: pyrepl: Include globals from modules passed with -i
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jun 22, 2024
1 parent a046c84 commit a6a26ff
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 61 deletions.
51 changes: 4 additions & 47 deletions Lib/_pyrepl/__main__.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,8 @@
import os
import sys
# Important: put as few things as possible in the global namespace of this module,
# as it's easy for things in this module's `__globals__` to accidentally end up
# in the globals of the REPL

CAN_USE_PYREPL: bool
if sys.platform != "win32":
CAN_USE_PYREPL = True
else:
CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2


def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
global CAN_USE_PYREPL
if not CAN_USE_PYREPL:
return sys._baserepl()

startup_path = os.getenv("PYTHONSTARTUP")
if pythonstartup and startup_path:
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code)

# set sys.{ps1,ps2} just before invoking the interactive interpreter. This
# mimics what CPython does in pythonrun.c
if not hasattr(sys, "ps1"):
sys.ps1 = ">>> "
if not hasattr(sys, "ps2"):
sys.ps2 = "... "

run_interactive = None
try:
import errno
if not os.isatty(sys.stdin.fileno()):
raise OSError(errno.ENOTTY, "tty required", "stdin")
from .simple_interact import check
if err := check():
raise RuntimeError(err)
from .simple_interact import run_multiline_interactive_console
run_interactive = run_multiline_interactive_console
except Exception as e:
from .trace import trace
msg = f"warning: can't use pyrepl: {e}"
trace(msg)
print(msg, file=sys.stderr)
CAN_USE_PYREPL = False
if run_interactive is None:
return sys._baserepl()
return run_interactive(mainmodule)
from ._main import interactive_console

if __name__ == "__main__":
interactive_console()
48 changes: 48 additions & 0 deletions Lib/_pyrepl/_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import sys

CAN_USE_PYREPL: bool
if sys.platform != "win32":
CAN_USE_PYREPL = True
else:
CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2


def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
global CAN_USE_PYREPL
if not CAN_USE_PYREPL:
return sys._baserepl()

startup_path = os.getenv("PYTHONSTARTUP")
if pythonstartup and startup_path:
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code)

# set sys.{ps1,ps2} just before invoking the interactive interpreter. This
# mimics what CPython does in pythonrun.c
if not hasattr(sys, "ps1"):
sys.ps1 = ">>> "
if not hasattr(sys, "ps2"):
sys.ps2 = "... "

run_interactive = None
try:
import errno
if not os.isatty(sys.stdin.fileno()):
raise OSError(errno.ENOTTY, "tty required", "stdin")
from .simple_interact import check
if err := check():
raise RuntimeError(err)
from .simple_interact import run_multiline_interactive_console
run_interactive = run_multiline_interactive_console
except Exception as e:
from .trace import trace
msg = f"warning: can't use pyrepl: {e}"
trace(msg)
print(msg, file=sys.stderr)
CAN_USE_PYREPL = False
if run_interactive is None:
return sys._baserepl()
return run_interactive(mainmodule)
27 changes: 17 additions & 10 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,30 @@ def _clear_screen():
"clear": _clear_screen,
}

DEFAULT_NAMESPACE: dict[str, Any] = {
'__name__': '__main__',
'__doc__': None,
'__package__': None,
'__loader__': None,
'__spec__': None,
'__annotations__': {},
'__builtins__': builtins,
}

def default_namespace() -> dict[str, Any]:
ns = {}
for key, value in sys.modules["__main__"].__dict__.items():
# avoid `getattr(value, "__module__", "")`,
# as some objects raise `TypeError` on attribute access, etc.
try:
module = value.__module__
except Exception:
pass
else:
if module.split(".")[0] == "_pyrepl":
continue
ns[key] = value
return ns


def run_multiline_interactive_console(
mainmodule: ModuleType | None = None,
future_flags: int = 0,
console: code.InteractiveConsole | None = None,
) -> None:
from .readline import _setup
namespace = mainmodule.__dict__ if mainmodule else DEFAULT_NAMESPACE
namespace = mainmodule.__dict__ if mainmodule else default_namespace()
_setup(namespace)

if console is None:
Expand Down
32 changes: 28 additions & 4 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import io
import itertools
import os
import pathlib
import rlcompleter
import select
import subprocess
import sys
import tempfile
from unittest import TestCase, skipUnless
from unittest.mock import patch
from test.support import force_not_colorized
Expand Down Expand Up @@ -844,15 +846,28 @@ class TestMain(TestCase):
@force_not_colorized
def test_exposed_globals_in_repl(self):
expected_output = (
"[\'__annotations__\', \'__builtins__\', \'__doc__\', \'__loader__\', "
"\'__name__\', \'__package__\', \'__spec__\']"
"['__annotations__', '__builtins__', '__cached__', '__doc__', "
"'__file__', '__loader__', '__name__', '__package__', '__spec__']"
)
output, exit_code = self.run_repl(["sorted(dir())", "exit"])
if "can\'t use pyrepl" in output:
self.skipTest("pyrepl not available")
self.assertEqual(exit_code, 0)
self.assertIn(expected_output, output)

@force_not_colorized
def test_globals_from_file_passed_included_in_repl_globals(self):
with tempfile.TemporaryDirectory() as td:
fake_main = pathlib.Path(td, "foo.py")
fake_main.write_text("FOO = 42", encoding="utf-8")
output, exit_code = self.run_repl(
["FOO", "exit"], main_module=str(fake_main)
)
if "can\'t use pyrepl" in output:
self.skipTest("pyrepl not available")
self.assertEqual(exit_code, 0)
self.assertIn("42", output)

def test_dumb_terminal_exits_cleanly(self):
env = os.environ.copy()
env.update({"TERM": "dumb"})
Expand All @@ -862,10 +877,19 @@ def test_dumb_terminal_exits_cleanly(self):
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)

def run_repl(self, repl_input: str | list[str], env: dict | None = None) -> tuple[str, int]:
def run_repl(
self,
repl_input: str | list[str],
env: dict | None = None,
*,
main_module: str | None = None
) -> tuple[str, int]:
master_fd, slave_fd = pty.openpty()
repl_args = [sys.executable, "-u", "-i"]
if main_module is not None:
repl_args.append(main_module)
process = subprocess.Popen(
[sys.executable, "-i", "-u"],
repl_args,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix regression in the new REPL that meant that globals from files passed
using the `-i` argument would not be included in the REPL's global
namespace. Patch by Alex Waygood.

0 comments on commit a6a26ff

Please sign in to comment.