Skip to content

Commit 3d9692d

Browse files
miss-islingtonAlexWaygoodambv
authored
[3.13] gh-120678: pyrepl: Include globals from modules passed with -i (GH-120904) (#121916)
(cherry picked from commit ac07451) Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 5a8e137 commit 3d9692d

File tree

6 files changed

+178
-11
lines changed

6 files changed

+178
-11
lines changed

Lib/_pyrepl/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Important: don't add things to this module, as they will end up in the REPL's
2+
# default globals. Use _pyrepl.main instead.
3+
14
if __name__ == "__main__":
25
from .main import interactive_console as __pyrepl_interactive_console
36
__pyrepl_interactive_console()

Lib/test/support/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2595,14 +2595,18 @@ def force_not_colorized(func):
25952595
def wrapper(*args, **kwargs):
25962596
import _colorize
25972597
original_fn = _colorize.can_colorize
2598-
variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
2598+
variables: dict[str, str | None] = {
2599+
"PYTHON_COLORS": None, "FORCE_COLOR": None, "NO_COLOR": None
2600+
}
25992601
try:
26002602
for key in variables:
26012603
variables[key] = os.environ.pop(key, None)
2604+
os.environ["NO_COLOR"] = "1"
26022605
_colorize.can_colorize = lambda: False
26032606
return func(*args, **kwargs)
26042607
finally:
26052608
_colorize.can_colorize = original_fn
2609+
del os.environ["NO_COLOR"]
26062610
for key, value in variables.items():
26072611
if value is not None:
26082612
os.environ[key] = value

Lib/test/test_pyrepl/support.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from code import InteractiveConsole
23
from functools import partial
34
from typing import Iterable
@@ -100,8 +101,18 @@ def handle_all_events(
100101
)
101102

102103

104+
def make_clean_env() -> dict[str, str]:
105+
clean_env = os.environ.copy()
106+
for k in clean_env.copy():
107+
if k.startswith("PYTHON"):
108+
clean_env.pop(k)
109+
clean_env.pop("FORCE_COLOR", None)
110+
clean_env.pop("NO_COLOR", None)
111+
return clean_env
112+
113+
103114
class FakeConsole(Console):
104-
def __init__(self, events, encoding="utf-8"):
115+
def __init__(self, events, encoding="utf-8") -> None:
105116
self.events = iter(events)
106117
self.encoding = encoding
107118
self.screen = []

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import itertools
33
import os
44
import pathlib
5+
import re
56
import rlcompleter
67
import select
78
import subprocess
@@ -21,7 +22,8 @@
2122
more_lines,
2223
multiline_input,
2324
code_to_events,
24-
clean_screen
25+
clean_screen,
26+
make_clean_env,
2527
)
2628
from _pyrepl.console import Event
2729
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
@@ -487,6 +489,18 @@ def prepare_reader(self, events):
487489
reader.can_colorize = False
488490
return reader
489491

492+
def test_stdin_is_tty(self):
493+
# Used during test log analysis to figure out if a TTY was available.
494+
if os.isatty(sys.stdin.fileno()):
495+
return
496+
self.skipTest("stdin is not a tty")
497+
498+
def test_stdout_is_tty(self):
499+
# Used during test log analysis to figure out if a TTY was available.
500+
if os.isatty(sys.stdout.fileno()):
501+
return
502+
self.skipTest("stdout is not a tty")
503+
490504
def test_basic(self):
491505
reader = self.prepare_reader(code_to_events("1+1\n"))
492506

@@ -888,12 +902,7 @@ def setUp(self):
888902
# Cleanup from PYTHON* variables to isolate from local
889903
# user settings, see #121359. Such variables should be
890904
# added later in test methods to patched os.environ.
891-
clean_env = os.environ.copy()
892-
for k in clean_env.copy():
893-
if k.startswith("PYTHON"):
894-
clean_env.pop(k)
895-
896-
patcher = patch('os.environ', new=clean_env)
905+
patcher = patch('os.environ', new=make_clean_env())
897906
self.addCleanup(patcher.stop)
898907
patcher.start()
899908

@@ -920,6 +929,84 @@ def test_exposed_globals_in_repl(self):
920929

921930
self.assertTrue(case1 or case2 or case3 or case4, output)
922931

932+
def _assertMatchOK(
933+
self, var: str, expected: str | re.Pattern, actual: str
934+
) -> None:
935+
if isinstance(expected, re.Pattern):
936+
self.assertTrue(
937+
expected.match(actual),
938+
f"{var}={actual} does not match {expected.pattern}",
939+
)
940+
else:
941+
self.assertEqual(
942+
actual,
943+
expected,
944+
f"expected {var}={expected}, got {var}={actual}",
945+
)
946+
947+
@force_not_colorized
948+
def _run_repl_globals_test(self, expectations, *, as_file=False, as_module=False):
949+
clean_env = make_clean_env()
950+
clean_env["NO_COLOR"] = "1" # force_not_colorized doesn't touch subprocesses
951+
952+
with tempfile.TemporaryDirectory() as td:
953+
blue = pathlib.Path(td) / "blue"
954+
blue.mkdir()
955+
mod = blue / "calx.py"
956+
mod.write_text("FOO = 42", encoding="utf-8")
957+
commands = [
958+
"print(f'{" + var + "=}')" for var in expectations
959+
] + ["exit"]
960+
if as_file and as_module:
961+
self.fail("as_file and as_module are mutually exclusive")
962+
elif as_file:
963+
output, exit_code = self.run_repl(
964+
commands,
965+
cmdline_args=[str(mod)],
966+
env=clean_env,
967+
)
968+
elif as_module:
969+
output, exit_code = self.run_repl(
970+
commands,
971+
cmdline_args=["-m", "blue.calx"],
972+
env=clean_env,
973+
cwd=td,
974+
)
975+
else:
976+
self.fail("Choose one of as_file or as_module")
977+
978+
if "can't use pyrepl" in output:
979+
self.skipTest("pyrepl not available")
980+
981+
self.assertEqual(exit_code, 0)
982+
for var, expected in expectations.items():
983+
with self.subTest(var=var, expected=expected):
984+
if m := re.search(rf"[\r\n]{var}=(.+?)[\r\n]", output):
985+
self._assertMatchOK(var, expected, actual=m.group(1))
986+
else:
987+
self.fail(f"{var}= not found in output")
988+
989+
self.assertNotIn("Exception", output)
990+
self.assertNotIn("Traceback", output)
991+
992+
def test_inspect_keeps_globals_from_inspected_file(self):
993+
expectations = {
994+
"FOO": "42",
995+
"__name__": "'__main__'",
996+
"__package__": "None",
997+
# "__file__" is missing in -i, like in the basic REPL
998+
}
999+
self._run_repl_globals_test(expectations, as_file=True)
1000+
1001+
def test_inspect_keeps_globals_from_inspected_module(self):
1002+
expectations = {
1003+
"FOO": "42",
1004+
"__name__": "'__main__'",
1005+
"__package__": "'blue'",
1006+
"__file__": re.compile(r"^'.*calx.py'$"),
1007+
}
1008+
self._run_repl_globals_test(expectations, as_module=True)
1009+
9231010
def test_dumb_terminal_exits_cleanly(self):
9241011
env = os.environ.copy()
9251012
env.update({"TERM": "dumb"})
@@ -981,16 +1068,27 @@ def test_not_wiping_history_file(self):
9811068
self.assertIn("spam", output)
9821069
self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0)
9831070

984-
def run_repl(self, repl_input: str | list[str], env: dict | None = None) -> tuple[str, int]:
1071+
def run_repl(
1072+
self,
1073+
repl_input: str | list[str],
1074+
env: dict | None = None,
1075+
*,
1076+
cmdline_args: list[str] | None = None,
1077+
cwd: str | None = None,
1078+
) -> tuple[str, int]:
1079+
assert pty
9851080
master_fd, slave_fd = pty.openpty()
9861081
cmd = [sys.executable, "-i", "-u"]
9871082
if env is None:
9881083
cmd.append("-I")
1084+
if cmdline_args is not None:
1085+
cmd.extend(cmdline_args)
9891086
process = subprocess.Popen(
9901087
cmd,
9911088
stdin=slave_fd,
9921089
stdout=slave_fd,
9931090
stderr=slave_fd,
1091+
cwd=cwd,
9941092
text=True,
9951093
close_fds=True,
9961094
env=env if env else os.environ,
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.

Modules/main.c

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pycore_call.h" // _PyObject_CallNoArgs()
55
#include "pycore_initconfig.h" // _PyArgv
66
#include "pycore_interp.h" // _PyInterpreterState.sysdict
7+
#include "pycore_long.h" // _PyLong_GetOne()
78
#include "pycore_pathconfig.h" // _PyPathConfig_ComputeSysPath0()
89
#include "pycore_pylifecycle.h" // _Py_PreInitializeFromPyArgv()
910
#include "pycore_pystate.h" // _PyInterpreterState_GET()
@@ -259,6 +260,53 @@ pymain_run_command(wchar_t *command)
259260
}
260261

261262

263+
static int
264+
pymain_start_pyrepl_no_main(void)
265+
{
266+
int res = 0;
267+
PyObject *pyrepl, *console, *empty_tuple, *kwargs, *console_result;
268+
pyrepl = PyImport_ImportModule("_pyrepl.main");
269+
if (pyrepl == NULL) {
270+
fprintf(stderr, "Could not import _pyrepl.main\n");
271+
res = pymain_exit_err_print();
272+
goto done;
273+
}
274+
console = PyObject_GetAttrString(pyrepl, "interactive_console");
275+
if (console == NULL) {
276+
fprintf(stderr, "Could not access _pyrepl.main.interactive_console\n");
277+
res = pymain_exit_err_print();
278+
goto done;
279+
}
280+
empty_tuple = PyTuple_New(0);
281+
if (empty_tuple == NULL) {
282+
res = pymain_exit_err_print();
283+
goto done;
284+
}
285+
kwargs = PyDict_New();
286+
if (kwargs == NULL) {
287+
res = pymain_exit_err_print();
288+
goto done;
289+
}
290+
if (!PyDict_SetItemString(kwargs, "pythonstartup", _PyLong_GetOne())) {
291+
_PyRuntime.signals.unhandled_keyboard_interrupt = 0;
292+
console_result = PyObject_Call(console, empty_tuple, kwargs);
293+
if (!console_result && PyErr_Occurred() == PyExc_KeyboardInterrupt) {
294+
_PyRuntime.signals.unhandled_keyboard_interrupt = 1;
295+
}
296+
if (console_result == NULL) {
297+
res = pymain_exit_err_print();
298+
}
299+
}
300+
done:
301+
Py_XDECREF(console_result);
302+
Py_XDECREF(kwargs);
303+
Py_XDECREF(empty_tuple);
304+
Py_XDECREF(console);
305+
Py_XDECREF(pyrepl);
306+
return res;
307+
}
308+
309+
262310
static int
263311
pymain_run_module(const wchar_t *modname, int set_argv0)
264312
{
@@ -549,7 +597,7 @@ pymain_repl(PyConfig *config, int *exitcode)
549597
*exitcode = (run != 0);
550598
return;
551599
}
552-
int run = pymain_run_module(L"_pyrepl", 0);
600+
int run = pymain_start_pyrepl_no_main();
553601
*exitcode = (run != 0);
554602
return;
555603
}

0 commit comments

Comments
 (0)