Skip to content

Commit acc8f82

Browse files
authored
fix: Allow PYTHONSTARTUP to define variables (bazel-contrib#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy