Skip to content

Commit acc8f82

Browse files
authored
fix: Allow PYTHONSTARTUP to define variables (#2911)
With the current `//python/bin:repl` implementation, any variables defined in `PYTHONSTARTUP` are not actually available in the REPL itself. I accidentally omitted this in the first patch. This patch fixes the issue and adds appropriate tests.
1 parent 9de326e commit acc8f82

File tree

3 files changed

+63
-4
lines changed

3 files changed

+63
-4
lines changed

python/bin/repl_stub.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
The logic for PYTHONSTARTUP is handled in python/private/repl_template.py.
1414
"""
1515

16+
# Capture the globals from PYTHONSTARTUP so we can pass them on to the console.
17+
console_locals = globals().copy()
18+
1619
import code
1720
import sys
1821

@@ -26,4 +29,4 @@
2629
sys.ps2 = ""
2730

2831
# We set the banner to an empty string because the repl_template.py file already prints the banner.
29-
code.interact(banner="", exitmsg=exitmsg)
32+
code.interact(local=console_locals, banner="", exitmsg=exitmsg)

python/private/repl_template.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def start_repl():
1414
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
1515
sys.stderr.write("Python %s on %s\n%s\n" % (sys.version, sys.platform, cprt))
1616

17+
# If there's a PYTHONSTARTUP script, we need to capture the new variables
18+
# that it defines.
19+
new_globals = {}
20+
1721
# Simulate Python's behavior when a valid startup script is defined by the
1822
# PYTHONSTARTUP variable. If this file path fails to load, print the error
1923
# and revert to the default behavior.
@@ -27,10 +31,14 @@ def start_repl():
2731
print(f"{type(error).__name__}: {error}")
2832
else:
2933
compiled_code = compile(source_code, filename=startup_file, mode="exec")
30-
eval(compiled_code, {})
34+
eval(compiled_code, new_globals)
3135

3236
bazel_runfiles = runfiles.Create()
33-
runpy.run_path(bazel_runfiles.Rlocation(STUB_PATH), run_name="__main__")
37+
runpy.run_path(
38+
bazel_runfiles.Rlocation(STUB_PATH),
39+
init_globals=new_globals,
40+
run_name="__main__",
41+
)
3442

3543

3644
if __name__ == "__main__":

tests/repl/repl_test.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22
import subprocess
33
import sys
4+
import tempfile
45
import unittest
6+
from pathlib import Path
57
from typing import Iterable
68

79
from python import runfiles
@@ -13,18 +15,26 @@
1315
EXPECT_TEST_MODULE_IMPORTABLE = os.environ["EXPECT_TEST_MODULE_IMPORTABLE"] == "1"
1416

1517

18+
# An arbitrary piece of code that sets some kind of variable. The variable needs to persist into the
19+
# actual shell.
20+
PYTHONSTARTUP_SETS_VAR = """\
21+
foo = 1234
22+
"""
23+
24+
1625
class ReplTest(unittest.TestCase):
1726
def setUp(self):
1827
self.repl = rfiles.Rlocation("rules_python/python/bin/repl")
1928
assert self.repl
2029

21-
def run_code_in_repl(self, lines: Iterable[str]) -> str:
30+
def run_code_in_repl(self, lines: Iterable[str], *, env=None) -> str:
2231
"""Runs the lines of code in the REPL and returns the text output."""
2332
return subprocess.check_output(
2433
[self.repl],
2534
text=True,
2635
stderr=subprocess.STDOUT,
2736
input="\n".join(lines),
37+
env=env,
2838
).strip()
2939

3040
def test_repl_version(self):
@@ -69,6 +79,44 @@ def test_import_test_module_failure(self):
6979
)
7080
self.assertIn("ModuleNotFoundError: No module named 'test_module'", result)
7181

82+
def test_pythonstartup_gets_executed(self):
83+
"""Validates that we can use the variables from PYTHONSTARTUP in the console itself."""
84+
with tempfile.TemporaryDirectory() as tempdir:
85+
pythonstartup = Path(tempdir) / "pythonstartup.py"
86+
pythonstartup.write_text(PYTHONSTARTUP_SETS_VAR)
87+
88+
env = os.environ.copy()
89+
env["PYTHONSTARTUP"] = str(pythonstartup)
90+
91+
result = self.run_code_in_repl(
92+
[
93+
"print(f'The value of foo is {foo}')",
94+
],
95+
env=env,
96+
)
97+
98+
self.assertIn("The value of foo is 1234", result)
99+
100+
def test_pythonstartup_doesnt_leak(self):
101+
"""Validates that we don't accidentally leak code into the console.
102+
103+
This test validates that a few of the variables we use in the template and stub are not
104+
accessible in the REPL itself.
105+
"""
106+
with tempfile.TemporaryDirectory() as tempdir:
107+
pythonstartup = Path(tempdir) / "pythonstartup.py"
108+
pythonstartup.write_text(PYTHONSTARTUP_SETS_VAR)
109+
110+
env = os.environ.copy()
111+
env["PYTHONSTARTUP"] = str(pythonstartup)
112+
113+
for var_name in ("exitmsg", "sys", "code", "bazel_runfiles", "STUB_PATH"):
114+
with self.subTest(var_name=var_name):
115+
result = self.run_code_in_repl([f"print({var_name})"], env=env)
116+
self.assertIn(
117+
f"NameError: name '{var_name}' is not defined", result
118+
)
119+
72120

73121
if __name__ == "__main__":
74122
unittest.main()

0 commit comments

Comments
 (0)