Skip to content

Commit 34cd838

Browse files
committed
Add support for REPLs
This patch adds three different ways to invoke a REPL for a given target, each with slightly unique use cases. Deployed binaries: Sometimes it's really useful to start the REPL for a binary that's already deployed in a docker container. You can do this with the `RULES_PYTHON_BOOTSTRAP_REPL` environment variable. For example: $ RULES_PYTHON_BOOTSTRAP_REPL=1 bazel run --//python/config_settings:bootstrap_impl=script //tools:wheelmaker Python 3.11.1 (main, Jan 16 2023, 22:41:20) [Clang 15.0.7 ] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import tools.wheelmaker >>> `py_{library,binary,test}` targets: These targets will now auto-generate additional `<name>.repl` targets. $ bazel run --//python/config_settings:bootstrap_impl=script //python/runfiles:runfiles.repl Python 3.11.1 (main, Jan 16 2023, 22:41:20) [Clang 15.0.7 ] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import python.runfiles >>> Arbitrary `PyInfo` providers: Spawn a REPL for any target that provides `PyInfo` like this: $ bazel run --//python/config_settings:bootstrap_impl=script //python/bin:repl --//python/bin:repl_dep=//tools:wheelmaker Python 3.11.1 (main, Jan 16 2023, 22:41:20) [Clang 15.0.7 ] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import tools.wheelmaker >>>
1 parent 1f8659c commit 34cd838

File tree

7 files changed

+102
-3
lines changed

7 files changed

+102
-3
lines changed

.bazelrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ build:rtd --enable_bzlmod
3636
common --incompatible_python_disallow_native_rules
3737

3838
build --lockfile_mode=update
39+
40+
run:repl --//python/config_settings:bootstrap_impl=script //python/bin:repl

python/bin/BUILD.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("//python/private:interpreter.bzl", _interpreter_binary = "interpreter_binary")
2+
load("//python:py_binary.bzl", "py_binary")
23

34
filegroup(
45
name = "distribution",
@@ -22,3 +23,28 @@ label_flag(
2223
name = "python_src",
2324
build_setting_default = "//python:none",
2425
)
26+
27+
py_binary(
28+
name = "repl",
29+
srcs = ["repl.py"],
30+
deps = [
31+
":repl_dep",
32+
":repl_lib_dep",
33+
],
34+
visibility = ["//visibility:public"],
35+
)
36+
37+
# The user can modify this flag to make arbitrary libraries available for import
38+
# on the REPL. Anything that exposes PyInfo can be used here.
39+
label_flag(
40+
name = "repl_dep",
41+
build_setting_default = "//python:none",
42+
)
43+
44+
# The user can modify this flag to make additional libraries available
45+
# specifically for the purpose of interacting with the REPL. For example, point
46+
# this at ipython in your .bazelrc file.
47+
label_flag(
48+
name = "repl_lib_dep",
49+
build_setting_default = "//python:none",
50+
)

python/bin/repl.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
from pathlib import Path
3+
4+
def start_repl():
5+
# Simulate Python's behavior when a valid startup script is defined by the
6+
# PYTHONSTARTUP variable. If this file path fails to load, print the error
7+
# and revert to the default behavior.
8+
if (startup_file := os.getenv("PYTHONSTARTUP")):
9+
try:
10+
source_code = Path(startup_file).read_text()
11+
except Exception as error:
12+
print(f"{type(error).__name__}: {error}")
13+
else:
14+
compiled_code = compile(source_code, filename=startup_file, mode="exec")
15+
eval(compiled_code, {})
16+
17+
try:
18+
# If the user has made ipython available somehow (e.g. via
19+
# `repl_lib_dep`), then use it.
20+
import IPython
21+
IPython.start_ipython()
22+
except ModuleNotFoundError:
23+
# Fall back to the default shell.
24+
import code
25+
code.interact(local=dict(globals(), **locals()))
26+
27+
if __name__ == "__main__":
28+
start_repl()

python/private/py_library_macro.bzl

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,35 @@
1414
"""Implementation of macro-half of py_library rule."""
1515

1616
load(":py_library_rule.bzl", py_library_rule = "py_library")
17+
load(":py_binary_rule.bzl", py_binary_rule = "py_binary")
18+
19+
# The py_library's attributes we don't want to forward to auto-generated
20+
# targets.
21+
_LIBRARY_ONLY_ATTRS = [
22+
"srcs",
23+
"deps",
24+
"data",
25+
"imports",
26+
]
1727

1828
# A wrapper macro is used to avoid any user-observable changes between a
1929
# rule and macro. It also makes generator_function look as expected.
20-
def py_library(**kwargs):
21-
py_library_rule(**kwargs)
30+
def py_library(name, **kwargs):
31+
library_only_attrs = {
32+
attr: kwargs.pop(attr, None)
33+
for attr in _LIBRARY_ONLY_ATTRS
34+
}
35+
py_library_rule(
36+
name = name,
37+
**(library_only_attrs | kwargs)
38+
)
39+
py_binary_rule(
40+
name = "%s.repl" % name,
41+
srcs = [],
42+
main_module = "python.bin.repl",
43+
deps = [
44+
":%s" % name,
45+
"@rules_python//python/bin:repl",
46+
],
47+
**kwargs
48+
)

python/private/sentinel.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Label attributes with defaults cannot accept None, otherwise they fall
1818
back to using the default. A sentinel allows detecting an intended None value.
1919
"""
2020

21+
load("//python:py_info.bzl", "PyInfo")
22+
2123
SentinelInfo = provider(
2224
doc = "Indicates this was the sentinel target.",
2325
fields = [],
@@ -29,6 +31,7 @@ def _sentinel_impl(ctx):
2931
SentinelInfo(),
3032
# Also output ToolchainInfo to allow it to be used for noop toolchains
3133
platform_common.ToolchainInfo(),
34+
PyInfo(transitive_sources=depset()),
3235
]
3336

3437
sentinel = rule(implementation = _sentinel_impl)

python/private/stage1_bootstrap_template.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ command=(
238238
"$@"
239239
)
240240

241+
# Point libedit/readline at the correct terminfo databases.
242+
# https://github.com/astral-sh/python-build-standalone/blob/f0abfc9cb1f6a985fc5561cf5435f7f6e8a64e5b/docs/quirks.rst#backspace-key-doesnt-work-in-python-repl
243+
export TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo
244+
241245
# We use `exec` instead of a child process so that signals sent directly (e.g.
242246
# using `kill`) to this process (the PID seen by the calling process) are
243247
# received by the Python process. Otherwise, this process receives the signal

python/private/stage2_bootstrap_template.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,16 @@ def main():
365365
print_verbose("initial environ:", mapping=os.environ)
366366
print_verbose("initial sys.path:", values=sys.path)
367367

368+
if bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_REPL")):
369+
global MAIN_PATH
370+
global MAIN_MODULE
371+
MAIN_PATH = ""
372+
# TODO(philsc): Can we point at python.bin.repl instead? That would mean
373+
# adding it as a dependency to all binaries.
374+
MAIN_MODULE = "code"
375+
# Prevent subprocesses from also entering the REPL.
376+
del os.environ["RULES_PYTHON_BOOTSTRAP_REPL"]
377+
368378
main_rel_path = None
369379
# todo: things happen to work because find_runfiles_root
370380
# ends up using stage2_bootstrap, and ends up computing the proper
@@ -438,7 +448,6 @@ def main():
438448
_run_py_path(main_filename, args=sys.argv[1:])
439449
else:
440450
_run_py_module(MAIN_MODULE)
441-
sys.exit(0)
442451

443452

444453
main()

0 commit comments

Comments
 (0)