Skip to content

Commit f5289c4

Browse files
[3.13] gh-118908: Limit exposed globals from internal imports and definitions on new REPL startup (GH-119547) (#120362)
1 parent 51bcb67 commit f5289c4

File tree

4 files changed

+83
-8
lines changed

4 files changed

+83
-8
lines changed

Lib/_pyrepl/simple_interact.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@
2727

2828
import _sitebuiltins
2929
import linecache
30+
import builtins
3031
import sys
3132
import code
3233
from types import ModuleType
3334

3435
from .console import InteractiveColoredConsole
3536
from .readline import _get_reader, multiline_input
3637

38+
TYPE_CHECKING = False
39+
40+
if TYPE_CHECKING:
41+
from typing import Any
42+
43+
3744
_error: tuple[type[Exception], ...] | type[Exception]
3845
try:
3946
from .unix_console import _error
@@ -73,20 +80,28 @@ def _clear_screen():
7380
"clear": _clear_screen,
7481
}
7582

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+
}
7692

7793
def run_multiline_interactive_console(
7894
mainmodule: ModuleType | None = None,
7995
future_flags: int = 0,
8096
console: code.InteractiveConsole | None = None,
8197
) -> None:
82-
import __main__
8398
from .readline import _setup
8499
_setup()
85100

86-
mainmodule = mainmodule or __main__
101+
namespace = mainmodule.__dict__ if mainmodule else DEFAULT_NAMESPACE
87102
if console is None:
88103
console = InteractiveColoredConsole(
89-
mainmodule.__dict__, filename="<stdin>"
104+
namespace, filename="<stdin>"
90105
)
91106
if future_flags:
92107
console.compile.compiler.flags |= future_flags

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import itertools
21
import io
2+
import itertools
33
import os
44
import rlcompleter
5-
from unittest import TestCase
5+
import select
6+
import subprocess
7+
import sys
8+
from unittest import TestCase, skipUnless
69
from unittest.mock import patch
10+
from test.support import force_not_colorized
711

812
from .support import (
913
FakeConsole,
@@ -17,6 +21,10 @@
1721
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
1822
from _pyrepl.readline import multiline_input as readline_multiline_input
1923

24+
try:
25+
import pty
26+
except ImportError:
27+
pty = None
2028

2129
class TestCursorPosition(TestCase):
2230
def prepare_reader(self, events):
@@ -828,3 +836,54 @@ def test_bracketed_paste_single_line(self):
828836
reader = self.prepare_reader(events)
829837
output = multiline_input(reader)
830838
self.assertEqual(output, input_code)
839+
840+
841+
@skipUnless(pty, "requires pty")
842+
class TestMain(TestCase):
843+
@force_not_colorized
844+
def test_exposed_globals_in_repl(self):
845+
expected_output = (
846+
"[\'__annotations__\', \'__builtins__\', \'__doc__\', \'__loader__\', "
847+
"\'__name__\', \'__package__\', \'__spec__\']"
848+
)
849+
output, exit_code = self.run_repl(["sorted(dir())", "exit"])
850+
if "can\'t use pyrepl" in output:
851+
self.skipTest("pyrepl not available")
852+
self.assertEqual(exit_code, 0)
853+
self.assertIn(expected_output, output)
854+
855+
def test_dumb_terminal_exits_cleanly(self):
856+
env = os.environ.copy()
857+
env.update({"TERM": "dumb"})
858+
output, exit_code = self.run_repl("exit()\n", env=env)
859+
self.assertEqual(exit_code, 0)
860+
self.assertIn("warning: can\'t use pyrepl", output)
861+
self.assertNotIn("Exception", output)
862+
self.assertNotIn("Traceback", output)
863+
864+
def run_repl(self, repl_input: str | list[str], env: dict | None = None) -> tuple[str, int]:
865+
master_fd, slave_fd = pty.openpty()
866+
process = subprocess.Popen(
867+
[sys.executable, "-i", "-u"],
868+
stdin=slave_fd,
869+
stdout=slave_fd,
870+
stderr=slave_fd,
871+
text=True,
872+
close_fds=True,
873+
env=env if env else os.environ,
874+
)
875+
if isinstance(repl_input, list):
876+
repl_input = "\n".join(repl_input) + "\n"
877+
os.write(master_fd, repl_input.encode("utf-8"))
878+
879+
output = []
880+
while select.select([master_fd], [], [], 0.5)[0]:
881+
data = os.read(master_fd, 1024).decode("utf-8")
882+
if not data:
883+
break
884+
output.append(data)
885+
886+
os.close(master_fd)
887+
os.close(slave_fd)
888+
exit_code = process.wait()
889+
return "\n".join(output), exit_code

Lib/test/test_repl.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Test the interactive interpreter."""
22

3-
import sys
43
import os
5-
import unittest
64
import subprocess
5+
import sys
6+
import unittest
77
from textwrap import dedent
88
from test import support
99
from test.support import cpython_only, has_subprocess_support, SuppressCrashReport
@@ -199,7 +199,6 @@ def test_asyncio_repl_is_ok(self):
199199
assert_python_ok("-m", "asyncio")
200200

201201

202-
203202
class TestInteractiveModeSyntaxErrors(unittest.TestCase):
204203

205204
def test_interactive_syntax_error_correct_line(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Limit exposed globals from internal imports and definitions on new REPL
2+
startup. Patch by Eugene Triguba and Pablo Galindo.

0 commit comments

Comments
 (0)