8000 gh-118908: Limit exposed globals from internal imports and definitions on new REPL startup by eugenetriguba · Pull Request #119547 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-118908: Limit exposed globals from internal imports and definitions on new REPL startup #119547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
46 changes: 1 addition & 45 deletions Lib/_pyrepl/__main__.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,3 @@
import os
import sys

CAN_USE_PYREPL = sys.platform != "win32"


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)

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

CAN_USE_PYREPL = sys.platform != "win32"


def interactive_console():
global CAN_USE_PYREPL
if not CAN_USE_PYREPL:
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)
CAN_USE_PYREPL = False
if run_interactive is None:
return sys._baserepl()
return run_interactive()
2 changes: 1 addition & 1 deletion Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def runsource(self, source, filename="<input>", symbol="single"):


def run_multiline_interactive_console(
mainmodule: ModuleType | None= None, future_flags: int = 0
mainmodule: ModuleType | None = None, future_flags: int = 0
) -> None:
import __main__
from .readline import _setup
Expand Down
6 changes: 1 addition & 5 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,11 +525,7 @@ 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 CAN_USE_PYREPL

try:
if os.getenv("PYTHON_BASIC_REPL") or not CAN_USE_PYREPL:
Expand Down
55 changes: 54 additions & 1 deletion Lib/test/test_pyrepl/test_pyrepl.py
2364
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import itertools
import importlib
import io
import itertools
import os
import pty
import rlcompleter
import select
import subprocess
import sys
import termios
from unittest import TestCase
from unittest.mock import patch

Expand Down Expand Up @@ -748,3 +754,50 @@ def test_bracketed_paste_single_line(self):
reader = self.prepare_reader(events)
output = multiline_input(reader)
self.assertEqual(output, input_code)


class TestMain(TestCase):
def test_exposed_globals_in_repl(self):
expected_output = (
"['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', "
"'__loader__', '__name__', '__package__', '__spec__', 'interactive_console']"
)
output, exit_code = self.run_repl(["dir()", "exit"])
self.assertEqual(exit_code, 0)
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]:
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
0