-
Notifications
You must be signed in to change notification settings - Fork 559
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(py_console_script_binary)!: entry points with custom dependencies (
#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 #1362 Fixes #543 Fixes #979 Fixes #1262 Closes #980 Closes #1294 Closes #1055 --------- Co-authored-by: Richard Levasseur <[email protected]>
- Loading branch information
Showing
33 changed files
with
1,201 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
load("@python_versions//3.9:defs.bzl", py_console_script_binary_3_9 = "py_console_script_binary") | ||
load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") | ||
|
||
# This is how you can define a `pylint` entrypoint which uses the default python version. | ||
py_console_script_binary( | ||
name = "pylint", | ||
pkg = "@pip//pylint", | ||
visibility = ["//entry_points:__subpackages__"], | ||
) | ||
|
||
# We can also specify extra dependencies for the binary, which is useful for | ||
# tools like flake8, pylint, pytest, which have plugin discovery methods. | ||
py_console_script_binary( | ||
name = "pylint_with_deps", | ||
pkg = "@pip//pylint", | ||
# Because `pylint` has multiple console_scripts available, we have to | ||
# specify which we want if the name of the target name 'pylint_with_deps' | ||
# cannot be used to guess the entry_point script. | ||
script = "pylint", | ||
visibility = ["//entry_points:__subpackages__"], | ||
deps = [ | ||
# One can add extra dependencies to the entry point. | ||
"@pip//pylint_print", | ||
], | ||
) | ||
|
||
# A specific Python version can be forced by using the generated version-aware | ||
# wrappers, e.g. to force Python 3.9: | ||
py_console_script_binary_3_9( | ||
name = "yamllint", | ||
pkg = "@pip//yamllint:pkg", | ||
visibility = ["//entry_points:__subpackages__"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
load("@bazel_skylib//rules:run_binary.bzl", "run_binary") | ||
load("@rules_python//python:defs.bzl", "py_test") | ||
|
||
# Below are targets for testing the `py_console_script_binary` feature and are | ||
# not part of the example how to use the feature. | ||
|
||
# And a test that we can correctly run `pylint --version` | ||
py_test( | ||
name = "pylint_test", | ||
srcs = ["pylint_test.py"], | ||
data = ["//entry_points:pylint"], | ||
env = { | ||
"ENTRY_POINT": "$(rlocationpath //entry_points:pylint)", | ||
}, | ||
deps = ["@rules_python//python/runfiles"], | ||
) | ||
|
||
# Next run pylint on the file to generate a report. | ||
run_binary( | ||
name = "pylint_report", | ||
srcs = [ | ||
":file_with_pylint_errors.py", | ||
], | ||
outs = ["pylint_report.txt"], | ||
args = [ | ||
"--output-format=text:$(location pylint_report.txt)", | ||
"--load-plugins=pylint_print", | ||
# The `exit-zero` ensures that `run_binary` is successful even though there are lint errors. | ||
# We check the generated report in the test below. | ||
"--exit-zero", | ||
"$(location :file_with_pylint_errors.py)", | ||
], | ||
env = { | ||
# otherwise it may try to create ${HOME}/.cache/pylint | ||
"PYLINTHOME": "./.pylint_home", | ||
}, | ||
tool = "//entry_points:pylint_with_deps", | ||
) | ||
|
||
py_test( | ||
name = "pylint_deps_test", | ||
srcs = ["pylint_deps_test.py"], | ||
data = [ | ||
":pylint_report", | ||
"//entry_points:pylint_with_deps", | ||
], | ||
env = { | ||
"ENTRY_POINT": "$(rlocationpath //entry_points:pylint_with_deps)", | ||
"PYLINT_REPORT": "$(rlocationpath :pylint_report)", | ||
}, | ||
deps = ["@rules_python//python/runfiles"], | ||
) | ||
|
||
# And a test to check that yamllint works | ||
py_test( | ||
name = "yamllint_test", | ||
srcs = ["yamllint_test.py"], | ||
data = ["//entry_points:yamllint"], | ||
env = { | ||
"ENTRY_POINT": "$(rlocationpath //entry_points:yamllint)", | ||
}, | ||
deps = ["@rules_python//python/runfiles"], | ||
) |
6 changes: 6 additions & 0 deletions
6
examples/bzlmod/entry_points/tests/file_with_pylint_errors.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
""" | ||
A file to demonstrate the pylint-print checker works. | ||
""" | ||
|
||
if __name__ == "__main__": | ||
print("Hello, World!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Copyright 2023 The Bazel Authors. All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import os | ||
import pathlib | ||
import subprocess | ||
import tempfile | ||
import unittest | ||
|
||
from python.runfiles import runfiles | ||
|
||
|
||
class ExampleTest(unittest.TestCase): | ||
def __init__(self, *args, **kwargs): | ||
self.maxDiff = None | ||
|
||
super().__init__(*args, **kwargs) | ||
|
||
def test_pylint_entry_point(self): | ||
rlocation_path = os.environ.get("ENTRY_POINT") | ||
assert ( | ||
rlocation_path is not None | ||
), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" | ||
|
||
entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) | ||
self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") | ||
|
||
# Let's run the entrypoint and check the tool version. | ||
# | ||
# NOTE @aignas 2023-08-24: the Windows python launcher with Python 3.9 and bazel 6 is not happy if we start | ||
# passing extra files via `subprocess.run` and it starts to fail with an error that the file which is the | ||
# entry_point cannot be found. However, just calling `--version` seems to be fine. | ||
proc = subprocess.run( | ||
[str(entry_point), "--version"], | ||
check=True, | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
) | ||
self.assertEqual( | ||
"", | ||
proc.stderr.decode("utf-8").strip(), | ||
) | ||
self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9") | ||
|
||
def test_pylint_report_has_expected_warnings(self): | ||
rlocation_path = os.environ.get("PYLINT_REPORT") | ||
assert ( | ||
rlocation_path is not None | ||
), "expected 'PYLINT_REPORT' env variable to be set to rlocation of the report" | ||
|
||
pylint_report = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) | ||
self.assertTrue(pylint_report.exists(), f"'{pylint_report}' does not exist") | ||
|
||
self.assertRegex( | ||
pylint_report.read_text().strip(), | ||
"W8201: Logging should be used instead of the print\(\) function\. \(print-function\)", | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Oops, something went wrong.