Skip to content

Commit 9818a60

Browse files
aignasrickeylev
andauthored
feat(py_console_script_binary)!: entry points with custom dependencies (bazel-contrib#1363)
Add `py_console_script_binary`, a macro/rule that allows better customization of how entry points are generated. Notable features of it are: * It allows passing in additional dependencies, which makes it easier for plugin dependencies to be added to tools such as pylint or sphinx. * The underlying `py_binary` rule can be passed in, allowing custom rules, such as the version-aware rules, to be used for the resulting binary. * Entry point generation is based upon a wheel's `entry_points.txt` file. This helps avoid loading external repositories unless they're actually used, allows entry points to have better version-aware support, and allows bzlmod to provide a supportable mechanism for entry points. Because the expected common use case is an entry point for our pip generated repos, there is special logic to make that easy and concisely do. Usage of `py_console_script_binary` is not tied to our pip code generation, though, and users can manually specify dependencies if they need to. BREAKING CHANGE: This is a breaking change, but only for bzlmod users. Note that bzlmod support is still beta. Bzlmod users will need to replace using `entry_point` from `requirements.bzl` with loading `py_console_script_binary` and defining the entry point locally: ``` load("@rules_python//python/entry_points:py_console_script_binary.bzl, "py_console_script_binary") py_console_script_binary(name="foo", pkg="@mypip//pylint") ``` For workspace users, this new macro is available to be used, but the old code is still present. Fixes bazel-contrib#1362 Fixes bazel-contrib#543 Fixes bazel-contrib#979 Fixes bazel-contrib#1262 Closes bazel-contrib#980 Closes bazel-contrib#1294 Closes bazel-contrib#1055 --------- Co-authored-by: Richard Levasseur <richardlev@gmail.com>
1 parent c32d232 commit 9818a60

33 files changed

+1201
-39
lines changed

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
# This lets us glob() up all the files inside the examples to make them inputs to tests
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
6-
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_point,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points
7-
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_point,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points
6+
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points
7+
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points
88

99
test --test_output=errors
1010

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ A brief description of the categories of changes:
1717
* Particular sub-systems are identified using parentheses, e.g. `(bzlmod)` or
1818
`(docs)`.
1919

20+
## Unreleased
21+
22+
### Added
23+
24+
* (bzlmod, entry_point) Added
25+
[`py_console_script_binary`](./docs/py_console_script_binary.md), which
26+
allows adding custom dependencies to a package's entry points and customizing
27+
the `py_binary` rule used to build it.
28+
29+
### Removed
30+
31+
* (bzlmod) The `entry_point` macro is no longer supported and has been removed
32+
in favour of the `py_console_script_binary` macro for `bzlmod` users.
33+
2034
## [0.25.0] - 2023-08-22
2135

2236
### Changed

docs/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ _DOCS = {
2727
"pip_repository": "//docs:pip-repository",
2828
"py_cc_toolchain": "//docs:py_cc_toolchain-docs",
2929
"py_cc_toolchain_info": "//docs:py_cc_toolchain_info-docs",
30+
"py_console_script_binary": "//docs:py-console-script-binary",
3031
"python": "//docs:core-docs",
3132
}
3233

@@ -128,6 +129,16 @@ stardoc(
128129
],
129130
)
130131

132+
stardoc(
133+
name = "py-console-script-binary",
134+
out = "py_console_script_binary.md_",
135+
input = "//python/entry_points:py_console_script_binary.bzl",
136+
target_compatible_with = _NOT_WINDOWS,
137+
deps = [
138+
"//python/entry_points:py_console_script_binary_bzl",
139+
],
140+
)
141+
131142
stardoc(
132143
name = "packaging-docs",
133144
out = "packaging.md_",

docs/py_console_script_binary.md

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/bzlmod/MODULE.bazel

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ pip.parse(
113113
"@whl_mods_hub//:wheel.json": "wheel",
114114
},
115115
)
116-
117-
# NOTE: The pip_39 repo is only used because the plain `@pip` repo doesn't
118-
# yet support entry points; see https://github.com/bazelbuild/rules_python/issues/1262
119-
use_repo(pip, "pip", "pip_39")
116+
use_repo(pip, "pip")
120117

121118
bazel_dep(name = "other_module", version = "", repo_name = "our_other_module")
122119
local_path_override(

examples/bzlmod/entry_point/BUILD.bazel

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
load("@python_versions//3.9:defs.bzl", py_console_script_binary_3_9 = "py_console_script_binary")
2+
load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
3+
4+
# This is how you can define a `pylint` entrypoint which uses the default python version.
5+
py_console_script_binary(
6+
name = "pylint",
7+
pkg = "@pip//pylint",
8+
visibility = ["//entry_points:__subpackages__"],
9+
)
10+
11+
# We can also specify extra dependencies for the binary, which is useful for
12+
# tools like flake8, pylint, pytest, which have plugin discovery methods.
13+
py_console_script_binary(
14+
name = "pylint_with_deps",
15+
pkg = "@pip//pylint",
16+
# Because `pylint` has multiple console_scripts available, we have to
17+
# specify which we want if the name of the target name 'pylint_with_deps'
18+
# cannot be used to guess the entry_point script.
19+
script = "pylint",
20+
visibility = ["//entry_points:__subpackages__"],
21+
deps = [
22+
# One can add extra dependencies to the entry point.
23+
"@pip//pylint_print",
24+
],
25+
)
26+
27+
# A specific Python version can be forced by using the generated version-aware
28+
# wrappers, e.g. to force Python 3.9:
29+
py_console_script_binary_3_9(
30+
name = "yamllint",
31+
pkg = "@pip//yamllint:pkg",
32+
visibility = ["//entry_points:__subpackages__"],
33+
)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
2+
load("@rules_python//python:defs.bzl", "py_test")
3+
4+
# Below are targets for testing the `py_console_script_binary` feature and are
5+
# not part of the example how to use the feature.
6+
7+
# And a test that we can correctly run `pylint --version`
8+
py_test(
9+
name = "pylint_test",
10+
srcs = ["pylint_test.py"],
11+
data = ["//entry_points:pylint"],
12+
env = {
13+
"ENTRY_POINT": "$(rlocationpath //entry_points:pylint)",
14+
},
15+
deps = ["@rules_python//python/runfiles"],
16+
)
17+
18+
# Next run pylint on the file to generate a report.
19+
run_binary(
20+
name = "pylint_report",
21+
srcs = [
22+
":file_with_pylint_errors.py",
23+
],
24+
outs = ["pylint_report.txt"],
25+
args = [
26+
"--output-format=text:$(location pylint_report.txt)",
27+
"--load-plugins=pylint_print",
28+
# The `exit-zero` ensures that `run_binary` is successful even though there are lint errors.
29+
# We check the generated report in the test below.
30+
"--exit-zero",
31+
"$(location :file_with_pylint_errors.py)",
32+
],
33+
env = {
34+
# otherwise it may try to create ${HOME}/.cache/pylint
35+
"PYLINTHOME": "./.pylint_home",
36+
},
37+
tool = "//entry_points:pylint_with_deps",
38+
)
39+
40+
py_test(
41+
name = "pylint_deps_test",
42+
srcs = ["pylint_deps_test.py"],
43+
data = [
44+
":pylint_report",
45+
"//entry_points:pylint_with_deps",
46+
],
47+
env = {
48+
"ENTRY_POINT": "$(rlocationpath //entry_points:pylint_with_deps)",
49+
"PYLINT_REPORT": "$(rlocationpath :pylint_report)",
50+
},
51+
deps = ["@rules_python//python/runfiles"],
52+
)
53+
54+
# And a test to check that yamllint works
55+
py_test(
56+
name = "yamllint_test",
57+
srcs = ["yamllint_test.py"],
58+
data = ["//entry_points:yamllint"],
59+
env = {
60+
"ENTRY_POINT": "$(rlocationpath //entry_points:yamllint)",
61+
},
62+
deps = ["@rules_python//python/runfiles"],
63+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
A file to demonstrate the pylint-print checker works.
3+
"""
4+
5+
if __name__ == "__main__":
6+
print("Hello, World!")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import pathlib
17+
import subprocess
18+
import tempfile
19+
import unittest
20+
21+
from python.runfiles import runfiles
22+
23+
24+
class ExampleTest(unittest.TestCase):
25+
def __init__(self, *args, **kwargs):
26+
self.maxDiff = None
27+
28+
super().__init__(*args, **kwargs)
29+
30+
def test_pylint_entry_point(self):
31+
rlocation_path = os.environ.get("ENTRY_POINT")
32+
assert (
33+
rlocation_path is not None
34+
), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool"
35+
36+
entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path))
37+
self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist")
38+
39+
# Let's run the entrypoint and check the tool version.
40+
#
41+
# NOTE @aignas 2023-08-24: the Windows python launcher with Python 3.9 and bazel 6 is not happy if we start
42+
# passing extra files via `subprocess.run` and it starts to fail with an error that the file which is the
43+
# entry_point cannot be found. However, just calling `--version` seems to be fine.
44+
proc = subprocess.run(
45+
[str(entry_point), "--version"],
46+
check=True,
47+
stdout=subprocess.PIPE,
48+
stderr=subprocess.PIPE,
49+
)
50+
self.assertEqual(
51+
"",
52+
proc.stderr.decode("utf-8").strip(),
53+
)
54+
self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9")
55+
56+
def test_pylint_report_has_expected_warnings(self):
57+
rlocation_path = os.environ.get("PYLINT_REPORT")
58+
assert (
59+
rlocation_path is not None
60+
), "expected 'PYLINT_REPORT' env variable to be set to rlocation of the report"
61+
62+
pylint_report = pathlib.Path(runfiles.Create().Rlocation(rlocation_path))
63+
self.assertTrue(pylint_report.exists(), f"'{pylint_report}' does not exist")
64+
65+
self.assertRegex(
66+
pylint_report.read_text().strip(),
67+
"W8201: Logging should be used instead of the print\(\) function\. \(print-function\)",
68+
)
69+
70+
71+
if __name__ == "__main__":
72+
unittest.main()

0 commit comments

Comments
 (0)