Skip to content

Commit e73dccf

Browse files
chrisirhcaignas
andauthored
feat: add shebang attribute on py_console_script_binary (#2867)
# Background Use case: user is setting up the environment for a docker image, and needs a bash executable from the py_console_script (e.g. to run `ray` from command line without full bazel bootstrapping). User is responsible of setting up the right paths (and hermeticity concerns). There's no change in default behavior per this diff. Previously, prior to Bazel mod, this was possible and simple through the use of `rules_python_wheel_entry_points` ([per here](https://github.com/bazel-contrib/rules_python/blob/9dfa3abba293488a9a1899832a340f7b44525cad/python/private/pypi/whl_library.bzl#L507)) but these are not reachable now via Bazel mod. # Approach Add a shebang attribute that allows users of the console binary to use it like a binary executable. This is similar to the functionality that came with wheel entry points here: https://github.com/bazel-contrib/rules_python/blob/9dfa3abba293488a9a1899832a340f7b44525cad/python/private/pypi/whl_library.bzl#L507 With this change, one can specify a shebang like: ```starlark py_console_script_binary( name = "yamllint", pkg = "@pip//yamllint", shebang = "#!/usr/bin/env python3", ) ``` Summary: - Update tests - Add test for this functionality - Leave default to without shebang so this is a non-breaking change - Documentation (want to hear more about the general approach first, and also want to hear whether this warrants specific docs, or can just leave it to API docs) --------- Co-authored-by: Ignas Anikevicius <[email protected]>
1 parent 28fda86 commit e73dccf

File tree

5 files changed

+75
-2
lines changed

5 files changed

+75
-2
lines changed

docs/_includes/py_console_script_binary.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ py_console_script_binary(
4848
)
4949
```
5050

51+
#### Adding a Shebang Line
52+
53+
You can specify a shebang line for the generated binary, useful for Unix-like
54+
systems where the shebang line determines which interpreter is used to execute
55+
the script, per [PEP441]:
56+
57+
```starlark
58+
load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
59+
60+
py_console_script_binary(
61+
name = "black",
62+
pkg = "@pip//black",
63+
shebang = "#!/usr/bin/env python3",
64+
)
65+
```
66+
67+
Note that to execute via the shebang line, you need to ensure the specified
68+
Python interpreter is available in the environment.
69+
70+
5171
#### Using a specific Python Version directly from a Toolchain
5272
:::{deprecated} 1.1.0
5373
The toolchain specific `py_binary` and `py_test` symbols are aliases to the regular rules.
@@ -70,4 +90,5 @@ py_console_script_binary(
7090
```
7191

7292
[specification]: https://packaging.python.org/en/latest/specifications/entry-points/
73-
[`py_console_script_binary.binary_rule`]: #py_console_script_binary_binary_rule
93+
[`py_console_script_binary.binary_rule`]: #py_console_script_binary_binary_rule
94+
[PEP441]: https://peps.python.org/pep-0441/#minimal-tooling-the-zipapp-module

python/private/py_console_script_binary.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def py_console_script_binary(
5252
entry_points_txt = None,
5353
script = None,
5454
binary_rule = py_binary,
55+
shebang = "",
5556
**kwargs):
5657
"""Generate a py_binary for a console_script entry_point.
5758
@@ -68,6 +69,8 @@ def py_console_script_binary(
6869
binary_rule: {type}`callable`, The rule/macro to use to instantiate
6970
the target. It's expected to behave like {obj}`py_binary`.
7071
Defaults to {obj}`py_binary`.
72+
shebang: {type}`str`, The shebang to use for the entry point python file.
73+
Defaults to empty string.
7174
**kwargs: Extra parameters forwarded to `binary_rule`.
7275
"""
7376
main = "rules_python_entry_point_{}.py".format(name)
@@ -81,6 +84,7 @@ def py_console_script_binary(
8184
out = main,
8285
console_script = script,
8386
console_script_guess = name,
87+
shebang = shebang,
8488
visibility = ["//visibility:private"],
8589
)
8690

python/private/py_console_script_gen.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def _py_console_script_gen_impl(ctx):
4242
args = ctx.actions.args()
4343
args.add("--console-script", ctx.attr.console_script)
4444
args.add("--console-script-guess", ctx.attr.console_script_guess)
45+
args.add("--shebang", ctx.attr.shebang)
4546
args.add(entry_points_txt)
4647
args.add(ctx.outputs.out)
4748

@@ -81,6 +82,10 @@ py_console_script_gen = rule(
8182
doc = "Output file location.",
8283
mandatory = True,
8384
),
85+
"shebang": attr.string(
86+
doc = "The shebang to use for the entry point python file.",
87+
default = "",
88+
),
8489
"_tool": attr.label(
8590
default = ":py_console_script_gen_py",
8691
executable = True,

python/private/py_console_script_gen.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
_ENTRY_POINTS_TXT = "entry_points.txt"
4545

4646
_TEMPLATE = """\
47-
import sys
47+
{shebang}import sys
4848
4949
# See @rules_python//python/private:py_console_script_gen.py for explanation
5050
if getattr(sys.flags, "safe_path", False):
@@ -87,13 +87,16 @@ def run(
8787
out: pathlib.Path,
8888
console_script: str,
8989
console_script_guess: str,
90+
shebang: str,
9091
):
9192
"""Run the generator
9293
9394
Args:
9495
entry_points: The entry_points.txt file to be parsed.
9596
out: The output file.
9697
console_script: The console_script entry in the entry_points.txt file.
98+
console_script_guess: The string used for guessing the console_script if it is not provided.
99+
shebang: The shebang to use for the entry point python file. Defaults to empty string (no shebang).
97100
"""
98101
config = EntryPointsParser()
99102
config.read(entry_points)
@@ -136,6 +139,7 @@ def run(
136139
with open(out, "w") as f:
137140
f.write(
138141
_TEMPLATE.format(
142+
shebang=f"{shebang}\n" if shebang else "",
139143
module=module,
140144
attr=attr,
141145
entry_point=entry_point,
@@ -154,6 +158,10 @@ def main():
154158
required=True,
155159
help="The string used for guessing the console_script if it is not provided.",
156160
)
161+
parser.add_argument(
162+
"--shebang",
163+
help="The shebang to use for the entry point python file.",
164+
)
157165
parser.add_argument(
158166
"entry_points",
159167
metavar="ENTRY_POINTS_TXT",
@@ -173,6 +181,7 @@ def main():
173181
out=args.out,
174182
console_script=args.console_script,
175183
console_script_guess=args.console_script_guess,
184+
shebang=args.shebang,
176185
)
177186

178187

tests/entry_points/py_console_script_gen_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def test_no_console_scripts_error(self):
4747
out=outfile,
4848
console_script=None,
4949
console_script_guess="",
50+
shebang="",
5051
)
5152

5253
self.assertEqual(
@@ -76,6 +77,7 @@ def test_no_entry_point_selected_error(self):
7677
out=outfile,
7778
console_script=None,
7879
console_script_guess="bar-baz",
80+
shebang="",
7981
)
8082

8183
self.assertEqual(
@@ -106,6 +108,7 @@ def test_incorrect_entry_point(self):
106108
out=outfile,
107109
console_script="baz",
108110
console_script_guess="",
111+
shebang="",
109112
)
110113

111114
self.assertEqual(
@@ -134,6 +137,7 @@ def test_a_single_entry_point(self):
134137
out=out,
135138
console_script=None,
136139
console_script_guess="foo",
140+
shebang="",
137141
)
138142

139143
got = out.read_text()
@@ -185,13 +189,43 @@ def test_a_second_entry_point_class_method(self):
185189
out=out,
186190
console_script="bar",
187191
console_script_guess="",
192+
shebang="",
188193
)
189194

190195
got = out.read_text()
191196

192197
self.assertRegex(got, "from foo\.baz import Bar")
193198
self.assertRegex(got, "sys\.exit\(Bar\.baz\(\)\)")
194199

200+
def test_shebang_included(self):
201+
with tempfile.TemporaryDirectory() as tmpdir:
202+
tmpdir = pathlib.Path(tmpdir)
203+
given_contents = (
204+
textwrap.dedent(
205+
"""
206+
[console_scripts]
207+
foo = foo.bar:baz
208+
"""
209+
).strip()
210+
+ "\n"
211+
)
212+
entry_points = tmpdir / "entry_points.txt"
213+
entry_points.write_text(given_contents)
214+
out = tmpdir / "foo.py"
215+
216+
shebang = "#!/usr/bin/env python3"
217+
run(
218+
entry_points=entry_points,
219+
out=out,
220+
console_script=None,
221+
console_script_guess="foo",
222+
shebang=shebang,
223+
)
224+
225+
got = out.read_text()
226+
227+
self.assertTrue(got.startswith(shebang + "\n"))
228+
195229

196230
if __name__ == "__main__":
197231
unittest.main()

0 commit comments

Comments
 (0)