Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Lib/_pyrepl/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
import sys

IS_SUPPORTED_PYREPL_PLATFORM = sys.platform != "win32"
IS_USING_PYREPL = False


def interactive_console():
global IS_USING_PYREPL
if not IS_SUPPORTED_PYREPL_PLATFORM:
return sys._baserepl()

startup_path = os.getenv("PYTHONSTARTUP")
if 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)
if run_interactive is None:
return sys._baserepl()
IS_USING_PYREPL = True
return run_interactive()
21 changes: 18 additions & 3 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@

import _sitebuiltins
import linecache
import builtins
import sys
import code
from types import ModuleType

from .console import InteractiveColoredConsole
from .readline import _get_reader, multiline_input

TYPE_CHECKING = False

if TYPE_CHECKING:
from typing import Any


_error: tuple[type[Exception], ...] | type[Exception]
try:
from .unix_console import _error
Expand Down Expand Up @@ -73,20 +80,28 @@ 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 run_multiline_interactive_console(
mainmodule: ModuleType | None = None,
future_flags: int = 0,
console: code.InteractiveConsole | None = None,
) -> None:
import __main__
from .readline import _setup
_setup()

mainmodule = mainmodule or __main__
namespace = mainmodule.__dict__ if mainmodule else DEFAULT_NAMESPACE
if console is None:
console = InteractiveColoredConsole(
mainmodule.__dict__, filename="<stdin>"
namespace, filename="<stdin>"
)
if future_flags:
console.compile.compiler.flags |= future_flags
Expand Down
8 changes: 2 additions & 6 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,14 +525,10 @@ def register_readline():
pass

def write_history():
try:
# _pyrepl.__main__ is executed as the __main__ module
from __main__ import CAN_USE_PYREPL
except ImportError:
CAN_USE_PYREPL = False
from _pyrepl.main import IS_USING_PYREPL

try:
if os.getenv("PYTHON_BASIC_REPL") or not CAN_USE_PYREPL:
if os.getenv("PYTHON_BASIC_REPL") or not IS_USING_PYREPL:
readline.write_history_file(history)
else:
_pyrepl.readline.write_history_file(history)
Expand Down
60 changes: 59 additions & 1 deletion Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import itertools
import importlib
import io
import itertools
import os
import rlcompleter
import select
import subprocess
import sys
from unittest import TestCase
from unittest.mock import patch
from test.support import force_not_colorized

from .support import (
FakeConsole,
Expand Down Expand Up @@ -828,3 +833,56 @@ def test_bracketed_paste_single_line(self):
reader = self.prepare_reader(events)
output = multiline_input(reader)
self.assertEqual(output, input_code)


class TestMain(TestCase):
@force_not_colorized
def test_exposed_globals_in_repl(self):
expected_output = (
'["__annotations__", "__builtins__", "__doc__", "__loader__", '
'"__name__", "__package__", "__spec__"]'
)
output, exit_code = self.run_repl(["sorted(dir())", "exit"])
self.assertEqual(exit_code, 0)
output = output.replace("\'", '"')
self.assertIn(expected_output, output)

def test_dumb_terminal_exits_cleanly(self):
env = os.environ.copy()
env.update({"TERM": "dumb"})
output, exit_code = self.run_repl("exit()\n", env=env)
self.assertEqual(exit_code, 0)
self.assertIn("warning: can\'t use pyrepl", output)
self.assertNotIn("Exception", output)
self.assertNotIn("Traceback", output)

def run_repl(self, repl_input: str | list[str], env: dict | None = None) -> tuple[str, int]:
try:
import pty
except ImportError:
self.skipTest("pty module not available")
master_fd, slave_fd = pty.openpty()
process = subprocess.Popen(
[sys.executable, "-i", "-u"],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
text=True,
close_fds=True,
env=env if env else os.environ,
)
if isinstance(repl_input, list):
repl_input = "\n".join(repl_input) + "\n"
os.write(master_fd, repl_input.encode("utf-8"))

output = []
while select.select([master_fd], [], [], 0.5)[0]:
data = os.read(master_fd, 1024).decode("utf-8")
if not data:
break
output.append(data)

os.close(master_fd)
os.close(slave_fd)
exit_code = process.wait()
return "\n".join(output), exit_code
5 changes: 2 additions & 3 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Test the interactive interpreter."""

import sys
import os
import unittest
import subprocess
import sys
import unittest
from textwrap import dedent
from test import support
from test.support import cpython_only, has_subprocess_support, SuppressCrashReport
Expand Down Expand Up @@ -199,7 +199,6 @@ def test_asyncio_repl_is_ok(self):
assert_python_ok("-m", "asyncio")



class TestInteractiveModeSyntaxErrors(unittest.TestCase):

def test_interactive_syntax_error_correct_line(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Limit exposed globals from internal imports and definitions on new REPL
startup