Skip to content

Commit

Permalink
feat(coverage): Register coverage.py to hermetic toolchains
Browse files Browse the repository at this point in the history
Summary:
- A script to update URLs and sha256 values used to download platform
  specific coverage.py wheels.
- Allow to specify `coverage_tool` attribute in `python_repository`.
- Register `coverage.py` when running `pip_install_dependencies`
  function.
- Ensure that `use_repo` includes coverage tool, so that `bzlmod` users
  can use it too.
- Have a boolean flag, which disables setting `coverage_tool` in the
  toolchain code.

Does not work/bad/ugly:
- Use coverage.py v6.5.0 because the latest has `types.py` in the
  package directory, which imports from Python's stdlib `types` [1].
  Somehow the Python interpreter is thinking that the
  `from types import FrameType` is referring to the currently
  interpreted file and everything breaks. I would have expected the
  package to use absolute imports and only attempt to import from
  `coverage.types` if we use `coverage.types` and not just a plain
  `types` import.
- The `multi_python_versions` example cannot show coverage for the more
  complex tests that are using `subprocess`. I am wondering if this is
  related to the fact that we are including `coverage.py` via the
  toolchain and not through other mechanisms [2].
- The `__init__.py` files in the root of the WORKSPACE in `bzlmod` is
  breaking, when running under `bazel coverage //:test`. However, it
  started working when I renamed `__init__.py` to `lib.py`. I am
  suspecting that this has to do with the fact that the layer of
  indirection that `coverage` introduces could be something to do with
  that.

Work towards bazelbuild#43.

[1]: https://github.com/nedbat/coveragepy/blob/master/coverage/types.py
[2]: https://bazel.build/configure/coverage
  • Loading branch information
aignas committed Jan 9, 2023
1 parent 9022291 commit 6b54226
Show file tree
Hide file tree
Showing 13 changed files with 428 additions and 5 deletions.
16 changes: 16 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,20 @@ use_repo(
"pypi__tomli",
"pypi__wheel",
"pypi__zipp",
# coverage_deps managed by running ./tools/update_coverage_deps.py <version>
"pypi__coverage_cp310_aarch64-apple-darwin",
"pypi__coverage_cp310_aarch64-unknown-linux-gnu",
"pypi__coverage_cp310_x86_64-apple-darwin",
"pypi__coverage_cp310_x86_64-pc-windows-msvc",
"pypi__coverage_cp310_x86_64-unknown-linux-gnu",
"pypi__coverage_cp38_aarch64-apple-darwin",
"pypi__coverage_cp38_aarch64-unknown-linux-gnu",
"pypi__coverage_cp38_x86_64-apple-darwin",
"pypi__coverage_cp38_x86_64-pc-windows-msvc",
"pypi__coverage_cp38_x86_64-unknown-linux-gnu",
"pypi__coverage_cp39_aarch64-apple-darwin",
"pypi__coverage_cp39_aarch64-unknown-linux-gnu",
"pypi__coverage_cp39_x86_64-apple-darwin",
"pypi__coverage_cp39_x86_64-pc-windows-msvc",
"pypi__coverage_cp39_x86_64-unknown-linux-gnu",
)
2 changes: 2 additions & 0 deletions examples/bzlmod/.bazelrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
common --experimental_enable_bzlmod

coverage --java_runtime_version=remotejdk_11
2 changes: 1 addition & 1 deletion examples/bzlmod/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ compile_pip_requirements(

py_library(
name = "lib",
srcs = ["__init__.py"],
srcs = ["lib.py"],
deps = [
requirement("pylint"),
requirement("tabulate"),
Expand Down
2 changes: 1 addition & 1 deletion examples/bzlmod/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __init__ import main
from lib import main

if __name__ == "__main__":
print(main([["A", 1], ["B", 2]]))
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/bzlmod/test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest

from __init__ import main
from lib import main


class ExampleTest(unittest.TestCase):
Expand Down
2 changes: 2 additions & 0 deletions examples/multi_python_versions/.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ test --test_output=errors
# Windows requires these for multi-python support:
build --enable_runfiles
startup --windows_enable_symlinks

coverage --java_runtime_version=remotejdk_11
7 changes: 6 additions & 1 deletion python/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ def _python_impl(module_ctx):
python = module_extension(
implementation = _python_impl,
tag_classes = {
"toolchain": tag_class(attrs = dict({"name": attr.string(mandatory = True), "python_version": attr.string(mandatory = True)})),
"toolchain": tag_class(
attrs = {
"name": attr.string(mandatory = True),
"python_version": attr.string(mandatory = True),
},
),
},
)

Expand Down
3 changes: 3 additions & 0 deletions python/pip_install/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
load("//python/private:coverage_deps.bzl", "install_coverage_deps")

# Avoid a load from @bazel_skylib repository as users don't necessarily have it installed
load("//third_party/github.com/bazelbuild/bazel-skylib/lib:versions.bzl", "versions")
Expand Down Expand Up @@ -132,3 +133,5 @@ def pip_install_dependencies():
type = "zip",
build_file_content = _GENERIC_WHEEL,
)

install_coverage_deps()
109 changes: 109 additions & 0 deletions python/private/coverage_deps.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Dependencies for various features of rules_python
"""

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")

# Update with './tools/update_coverage_deps.py <version>'
#START: managed by update_coverage_deps.py script
_coverage_deps = [
(
"pypi__coverage_cp310_aarch64-apple-darwin",
"https://files.pythonhosted.org/packages/89/a2/cbf599e50bb4be416e0408c4cf523c354c51d7da39935461a9687e039481/coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl",
"784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
),
(
"pypi__coverage_cp310_aarch64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/15/b0/3639d84ee8a900da0cf6450ab46e22517e4688b6cec0ba8ab6f8166103a2/coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
),
(
"pypi__coverage_cp310_x86_64-apple-darwin",
"https://files.pythonhosted.org/packages/c4/8d/5ec7d08f4601d2d792563fe31db5e9322c306848fec1e65ec8885927f739/coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl",
"ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
),
(
"pypi__coverage_cp310_x86_64-pc-windows-msvc",
"https://files.pythonhosted.org/packages/ae/a3/f45cb5d32de0751863945d22083c15eb8854bb53681b2e792f2066c629b9/coverage-6.5.0-cp310-cp310-win_amd64.whl",
"59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e",
),
(
"pypi__coverage_cp310_x86_64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/3c/7d/d5211ea782b193ab8064b06dc0cc042cf1a4ca9c93a530071459172c550f/coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
),
(
"pypi__coverage_cp38_aarch64-apple-darwin",
"https://files.pythonhosted.org/packages/07/82/79fa21ceca9a9b091eb3c67e27eb648dade27b2c9e1eb23af47232a2a365/coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl",
"2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
),
(
"pypi__coverage_cp38_aarch64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/40/3b/cd68cb278c4966df00158811ec1e357b9a7d132790c240fc65da57e10013/coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
),
(
"pypi__coverage_cp38_x86_64-apple-darwin",
"https://files.pythonhosted.org/packages/05/63/a789b462075395d34f8152229dccf92b25ca73eac05b3f6cd75fa5017095/coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl",
"d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
),
(
"pypi__coverage_cp38_x86_64-pc-windows-msvc",
"https://files.pythonhosted.org/packages/06/f1/5177428c35f331f118e964f727f79e3a3073a10271a644c8361d3cea8bfd/coverage-6.5.0-cp38-cp38-win_amd64.whl",
"7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6",
),
(
"pypi__coverage_cp38_x86_64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/bd/a0/e263b115808226fdb2658f1887808c06ac3f1b579ef5dda02309e0d54459/coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
),
(
"pypi__coverage_cp39_aarch64-apple-darwin",
"https://files.pythonhosted.org/packages/63/e9/f23e8664ec4032d7802a1cf920853196bcbdce7b56408e3efe1b2da08f3c/coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl",
"95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
),
(
"pypi__coverage_cp39_aarch64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/18/95/27f80dcd8273171b781a19d109aeaed7f13d78ef6d1e2f7134a5826fd1b4/coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
"b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
),
(
"pypi__coverage_cp39_x86_64-apple-darwin",
"https://files.pythonhosted.org/packages/ea/52/c08080405329326a7ff16c0dfdb4feefaa8edd7446413df67386fe1bbfe0/coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl",
"633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
),
(
"pypi__coverage_cp39_x86_64-pc-windows-msvc",
"https://files.pythonhosted.org/packages/b6/08/a88a9f3a11bb2d97c7a6719535a984b009728433838fbc65766488867c80/coverage-6.5.0-cp39-cp39-win_amd64.whl",
"fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987",
),
(
"pypi__coverage_cp39_x86_64-unknown-linux-gnu",
"https://files.pythonhosted.org/packages/6b/f2/919f0fdc93d3991ca074894402074d847be8ac1e1d78e7e9e1c371b69a6f/coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
),
]
#END: managed by update_coverage_deps.py script

def install_coverage_deps():
"""Register the dependency for the coverage dep.
"""
for name, url, sha256 in _coverage_deps:
maybe(
http_archive,
name = name,
build_file_content = """
load("@rules_python//python:defs.bzl", "py_library")
py_library(
name = "coverage",
srcs = ["coverage/__main__.py"],
data = glob(["coverage/*", "coverage/**/*.py"]),
visibility = ["//visibility:public"],
imports = ["."],
)
""",
sha256 = sha256,
type = "zip",
urls = [url],
)
37 changes: 36 additions & 1 deletion python/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ exports_files(["python", "{python_path}"])
py_runtime(
name = "py3_runtime",
files = [":files"],
coverage_tool = {coverage_tool},
interpreter = "{python_path}",
python_version = "PY3",
)
Expand All @@ -255,13 +256,15 @@ py_runtime_pair(
python_path = python_bin,
python_version = python_short_version,
python_version_nodot = python_short_version.replace(".", ""),
coverage_tool = rctx.attr.coverage_tool if rctx.attr.coverage_tool == None else "\"{}\"".format(rctx.attr.coverage_tool),
)
rctx.delete("python")
rctx.symlink(python_bin, "python")
rctx.file(STANDALONE_INTERPRETER_FILENAME, "# File intentionally left blank. Indicates that this is an interpreter repo created by rules_python.")
rctx.file("BUILD.bazel", build_content)

return {
"coverage_tool": rctx.attr.coverage_tool,
"distutils": rctx.attr.distutils,
"distutils_content": rctx.attr.distutils_content,
"ignore_root_user_error": rctx.attr.ignore_root_user_error,
Expand All @@ -278,6 +281,25 @@ python_repository = repository_rule(
_python_repository_impl,
doc = "Fetches the external tools needed for the Python toolchain.",
attrs = {
"coverage_tool": attr.label(
# Mirrors the definition at
# https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/python/py_runtime_rule.bzl
allow_files = False,
doc = """
This is a target to use for collecting code coverage information from `py_binary`
and `py_test` targets.
If set, the target must either produce a single file or be an executable target.
The path to the single file, or the executable if the target is executable,
determines the entry point for the python coverage tool. The target and its
runfiles will be added to the runfiles when coverage is enabled.
The entry point for the tool must be loadable by a Python interpreter (e.g. a
`.py` or `.pyc` file). It must accept the command line arguments
of coverage.py (https://coverage.readthedocs.io), at least including
the `run` and `lcov` subcommands.
""",
),
"distutils": attr.label(
allow_single_file = True,
doc = "A distutils.cfg file to be included in the Python installation. " +
Expand Down Expand Up @@ -338,6 +360,7 @@ def python_register_toolchains(
distutils = None,
distutils_content = None,
register_toolchains = True,
register_coverage_tool = True,
set_python_version_constraint = False,
tool_versions = TOOL_VERSIONS,
**kwargs):
Expand All @@ -356,9 +379,10 @@ def python_register_toolchains(
distutils: see the distutils attribute in the python_repository repository rule.
distutils_content: see the distutils_content attribute in the python_repository repository rule.
register_toolchains: Whether or not to register the downloaded toolchains.
register_coverage_tool: Whether or not to register the downloaded coverage tool to the toolchains.
set_python_version_constraint: When set to true, target_compatible_with for the toolchains will include a version constraint.
tool_versions: a dict containing a mapping of version with SHASUM and platform info. If not supplied, the defaults
in python/versions.bzl will be used
in python/versions.bzl will be used.
**kwargs: passed to each python_repositories call.
"""
base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL)
Expand All @@ -375,6 +399,16 @@ def python_register_toolchains(

(release_filename, url, strip_prefix) = get_release_url(platform, python_version, base_url, tool_versions)

# allow passing in a tool version
coverage_tool = None
coverage_tool = tool_versions[python_version].get("coverage_tool", {}).get(platform, None)
if register_coverage_tool and coverage_tool == None:
python_short_version = python_version.rpartition(".")[0]
coverage_tool = Label("@pypi__coverage_cp{python_version_nodot}_{platform}//:coverage".format(
python_version_nodot = python_short_version.replace(".", ""),
platform = platform,
))

python_repository(
name = "{name}_{platform}".format(
name = name,
Expand All @@ -388,6 +422,7 @@ def python_register_toolchains(
distutils = distutils,
distutils_content = distutils_content,
strip_prefix = strip_prefix,
coverage_tool = coverage_tool,
**kwargs
)
if register_toolchains:
Expand Down
15 changes: 15 additions & 0 deletions python/versions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ DEFAULT_RELEASE_BASE_URL = "https://github.com/indygreg/python-build-standalone/
# the hashes:
# bazel run //python/private:print_toolchains_checksums
#
# Note, to users looking at how to specify their tool versions, coverage_tool version for each
# interpreter can be specified by:
# "3.8.10": {
# "url": "20210506/cpython-{python_version}-{platform}-pgo+lto-20210506T0943.tar.zst",
# "sha256": {
# "x86_64-apple-darwin": "8d06bec08db8cdd0f64f4f05ee892cf2fcbc58cfb1dd69da2caab78fac420238",
# "x86_64-unknown-linux-gnu": "aec8c4c53373b90be7e2131093caa26063be6d9d826f599c935c0e1042af3355",
# },
# "coverage_tool": {
# "x86_64-apple-darwin": "<label_for_darwin>"",
# "x86_64-unknown-linux-gnu": "<label_for_linux>"",
# },
# "strip_prefix": "python",
# },
#
# buildifier: disable=unsorted-dict-items
TOOL_VERSIONS = {
"3.8.10": {
Expand Down
Loading

0 comments on commit 6b54226

Please sign in to comment.