Skip to content

Commit

Permalink
feat(py_console_script_binary)!: entry points with custom dependencies (
Browse files Browse the repository at this point in the history
#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
aignas and rickeylev authored Aug 25, 2023
1 parent c32d232 commit 9818a60
Show file tree
Hide file tree
Showing 33 changed files with 1,201 additions and 39 deletions.
4 changes: 2 additions & 2 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# This lets us glob() up all the files inside the examples to make them inputs to tests
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
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
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
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
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

test --test_output=errors

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ A brief description of the categories of changes:
* Particular sub-systems are identified using parentheses, e.g. `(bzlmod)` or
`(docs)`.

## Unreleased

### Added

* (bzlmod, entry_point) Added
[`py_console_script_binary`](./docs/py_console_script_binary.md), which
allows adding custom dependencies to a package's entry points and customizing
the `py_binary` rule used to build it.

### Removed

* (bzlmod) The `entry_point` macro is no longer supported and has been removed
in favour of the `py_console_script_binary` macro for `bzlmod` users.

## [0.25.0] - 2023-08-22

### Changed
Expand Down
11 changes: 11 additions & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ _DOCS = {
"pip_repository": "//docs:pip-repository",
"py_cc_toolchain": "//docs:py_cc_toolchain-docs",
"py_cc_toolchain_info": "//docs:py_cc_toolchain_info-docs",
"py_console_script_binary": "//docs:py-console-script-binary",
"python": "//docs:core-docs",
}

Expand Down Expand Up @@ -128,6 +129,16 @@ stardoc(
],
)

stardoc(
name = "py-console-script-binary",
out = "py_console_script_binary.md_",
input = "//python/entry_points:py_console_script_binary.bzl",
target_compatible_with = _NOT_WINDOWS,
deps = [
"//python/entry_points:py_console_script_binary_bzl",
],
)

stardoc(
name = "packaging-docs",
out = "packaging.md_",
Expand Down
87 changes: 87 additions & 0 deletions docs/py_console_script_binary.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions examples/bzlmod/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,7 @@ pip.parse(
"@whl_mods_hub//:wheel.json": "wheel",
},
)

# NOTE: The pip_39 repo is only used because the plain `@pip` repo doesn't
# yet support entry points; see https://github.com/bazelbuild/rules_python/issues/1262
use_repo(pip, "pip", "pip_39")
use_repo(pip, "pip")

bazel_dep(name = "other_module", version = "", repo_name = "our_other_module")
local_path_override(
Expand Down
20 changes: 0 additions & 20 deletions examples/bzlmod/entry_point/BUILD.bazel

This file was deleted.

33 changes: 33 additions & 0 deletions examples/bzlmod/entry_points/BUILD.bazel
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__"],
)
63 changes: 63 additions & 0 deletions examples/bzlmod/entry_points/tests/BUILD.bazel
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 examples/bzlmod/entry_points/tests/file_with_pylint_errors.py
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!")
72 changes: 72 additions & 0 deletions examples/bzlmod/entry_points/tests/pylint_deps_test.py
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()
Loading

0 comments on commit 9818a60

Please sign in to comment.