Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1909697
test(coverage): check the coverage collection with the bundled covera…
aignas Nov 7, 2026
e0351f6
fix: register the coverage dev toolchain with a full python version
aignas Nov 7, 2026
088bce4
test(coverage): check the covered and not covered lines in separate t…
aignas Nov 7, 2026
62be724
feat(toolchain): allow using a py_library as the coverage_tool
aignas Nov 8, 2026
18e6ef7
fix: use the extracted wheel files instead of a py_library
aignas Nov 8, 2026
6b8bbf1
Merge remote-tracking branch 'origin/aignas.test.coverage-collection'…
aignas Nov 8, 2026
08d2743
build(dev): add coverage to the dev dependencies
aignas Nov 8, 2026
0e78f1d
test(coverage): check the coverage collection with coverage from a pi…
aignas Nov 8, 2026
2f48dcb
test(coverage): simplify the coverage collection test setup
aignas Nov 8, 2026
3304042
test(coverage): make collect_code_coverage a bool py_reconfig attr
aignas Nov 8, 2026
4c7e034
Merge remote-tracking branch 'origin/aignas.test.coverage-collection'…
aignas Nov 8, 2026
924c4a8
test(coverage): use the bool collect_code_coverage for instrumented_pypi
aignas Nov 8, 2026
e388a75
docs: link the coverage_tool doc to the coverage docs
aignas Nov 8, 2026
35e08ea
test(coverage): use pytest_test for the coverage collection test
aignas Nov 8, 2026
cb1632f
Merge branch 'aignas.test.coverage-collection' into aignas.feat.cover…
aignas Nov 8, 2026
c9b66df
test(coverage): use pytest_test for the coverage from PyPI test
aignas Nov 8, 2026
a7894e2
Merge branch 'main' into aignas.feat.coverage-tool-py-library
aignas Nov 9, 2026
79d9d12
Merge branch 'main' into aignas.feat.coverage-tool-py-library
aignas Nov 9, 2026
197fcce
fix(toolchain): build the coverage_tool without the toolchain coverag…
aignas Nov 9, 2026
6f653a3
feat(toolchain): import the coverage package using the PyInfo imports
aignas Nov 9, 2026
1f8e9a8
feat(pypi): support target_platforms = ["*"] in pip.parse
aignas Nov 9, 2026
db5f8f9
Merge branch 'aignas.feat.pip-target-platforms-all' into aignas.feat.…
aignas Nov 9, 2026
a26b77c
feat(toolchain): use the rules_python_coverage hub as the bzlmod cove…
aignas Nov 9, 2026
c5cd90d
docs(coverage): prefer passing a py_library as the coverage_tool
aignas Nov 9, 2026
5149b91
feat(pypi): use the none-any wheel as a fallback with target_platform…
aignas Nov 9, 2026
059c494
feat(pypi): define default pip platforms for all toolchain platforms
aignas Nov 9, 2026
dbcb20e
Merge branch 'aignas.feat.pip-target-platforms-all' into aignas.feat.…
aignas Nov 9, 2026
914ef45
Merge branch 'aignas.feat.pip-default-platforms' into aignas.feat.cov…
aignas Nov 9, 2026
6073cd0
Revert "feat(pypi): use the none-any wheel as a fallback with target_…
aignas Nov 9, 2026
b36ae25
Merge branch 'aignas.feat.pip-target-platforms-all' into aignas.feat.…
aignas Nov 9, 2026
d499b58
chore: rename the news entry to use the PR number
aignas Nov 9, 2026
ce9caee
chore: rename the news entry to use the PR number
aignas Nov 9, 2026
008dbdc
Merge branch 'aignas.feat.pip-target-platforms-all' into aignas.feat.…
aignas Nov 9, 2026
ed8f876
Merge branch 'aignas.feat.pip-default-platforms' into aignas.feat.cov…
aignas Nov 9, 2026
f61d790
test(pypi): select platform specific wheels with target_platforms = […
aignas Nov 9, 2026
63fef3f
Merge branch 'aignas.feat.pip-target-platforms-all' into aignas.feat.…
aignas Nov 9, 2026
29cdabd
chore: update the vendored requirements for the new default platforms
aignas Nov 9, 2026
5fa06ff
Merge branch 'aignas.feat.pip-default-platforms' into aignas.feat.cov…
aignas Nov 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,30 @@ pip.dep(name = "pytest")
pip.dep(name = "pytest-bazel")
pip.dep(name = "pytest-randomly")
pip.dep(name = "pytest-shard")
use_repo(pip, "pypi", "rules_python_publish_deps")

# The default coverage tool for the hermetic toolchains, see
# `python.toolchain.configure_coverage_tool`. The versions that do not have a
# registered toolchain are ignored.
[
pip.parse(
download_only = True,
hub_name = "rules_python_coverage",
python_version = python_version,
target_platforms = ["*"],
uv_lock = "//python/private/coverage:uv.lock",
)
for python_version in [
"3.9",
"3.10",
"3.11",
"3.12",
"3.13",
"3.14",
"3.15",
]
]

use_repo(pip, "pypi", "rules_python_coverage", "rules_python_publish_deps")

bazel_dep(
name = "rules_rust",
Expand Down Expand Up @@ -416,6 +439,26 @@ dev_python.toolchain(
python_version = "3.12.16",
)

# For testing the coverage collection with the coverage tool from a PyPI hub.
# See //tests/coverage:coverage_from_pypi_test
dev_python.toolchain(
python_version = "3.13.13",
)

[
dev_python.single_version_platform_override(
coverage_tool = "@dev_pip//coverage",
platform = platform,
python_version = "3.13.13",
)
for platform in [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
]
]

# For testing an arbitrary runtime triggered by a custom flag.
# See //tests/toolchains:custom_platform_toolchain_test
dev_python.single_version_platform_override(
Expand Down
2 changes: 2 additions & 0 deletions dev/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dependencies = [
"sphinx_rtd_theme >=2.0", # uv insists on downgrading for some reason
"readthedocs-sphinx-ext",
"absl-py",
# Used to test the coverage collection with coverage from a PyPI hub.
"coverage",
"typing-extensions",
"sphinx-reredirects",
"pefile",
Expand Down
264 changes: 264 additions & 0 deletions dev/uv.lock

Large diffs are not rendered by default.

76 changes: 54 additions & 22 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@ when configuring toolchains.

## Enabling `rules_python` coverage support

Enabling the coverage support bundled with `rules_python` just requires setting an
argument when registering toolchains.
Enabling the coverage support provided by `rules_python` just requires setting
an argument when registering toolchains.

For Bzlmod:

```starlark
python.toolchain(
"@python3_14_toolchains//:all",
configure_coverage_tool = True,
python_version = "3.14",
)
```

This uses the `coverage` package from the `@rules_python_coverage` pip hub,
which `rules_python` defines from its own `uv.lock` file. The lock file has the
latest `coverage` version that supports each of the Python versions starting
from 3.9 and the wheels for all of the platforms configured with
{obj}`pip.default`, so the coverage tool works when cross-building too.

For WORKSPACE configuration:

```starlark
Expand All @@ -29,7 +35,7 @@ python_register_toolchains(
```

:::{note}
This will implicitly add the version of `coverage` bundled with
This will implicitly add the version of `coverage` provided by
`rules_python` to the dependencies of `py_test` rules when `bazel coverage` is
run. If a target already transitively depends on a different version of
`coverage`, then the behavior is undefined -- it is undefined which version comes
Expand All @@ -38,12 +44,12 @@ need to manually configure coverage (see below).
:::

:::{note}
The bundled `coverage` wheel set covers CPython 3.9 through 3.14 (with
freethreaded variants for 3.13+), and not every platform within that range.
When the interpreter that `bazel coverage` actually selects has no bundled
wheel, `configure_coverage_tool = True` produces no coverage tool and
`bazel coverage` emits empty lcov data; manually configure coverage (see
below) instead.
For WORKSPACE, the bundled `coverage` wheel set covers CPython 3.9 through
3.14 (with freethreaded variants for 3.13+), and not every platform within
that range. When the interpreter that `bazel coverage` actually selects has no
bundled wheel, `register_coverage_tool = True` produces no coverage tool and
`bazel coverage` emits empty lcov data; manually configure coverage (see below)
instead.

`py_runtime` warns when this happens. The warning is emitted during analysis,
for the runtime toolchain resolution selected and only when coverage is being
Expand All @@ -54,21 +60,47 @@ registered for but a given build never uses.
## Manually configuring coverage

To manually configure coverage support, you'll need to set the
`py_runtime.coverage_tool` attribute. This attribute is a target that specifies
the coverage entry point file and, optionally, client libraries that are added
to `py_test` targets. Typically, this would be a `filegroup` that looked like:
{attr}`py_runtime.coverage_tool` attribute. The preferred way is to pass a
`py_library` of the `coverage` package, e.g. `@pypi//coverage` from your
{obj}`pip.parse` hub. See {ref}`coverage-from-pypi`.

The important behaviors of the target are:

* It provides {obj}`PyInfo` (e.g. it is a `py_library`), in which case its
import paths are used to import the `coverage` package. This is the
preferred way.
* Otherwise, it provides a single output file OR it provides an executable
output; this output is treated as the coverage entry point.
* If it provides runfiles, then `runfiles.files` are included into `py_test`.

:::{deprecated} VERSION_NEXT_FEATURE
Using a target that does not provide {obj}`PyInfo`, e.g. a `filegroup` with
the coverage entry point file, will be deprecated in the next version. Use a
`py_library` instead.
:::

(coverage-from-pypi)=
### Using `coverage` from PyPI

To use the version of `coverage` that is locked in your requirements instead of
the one provided by `rules_python`, point the toolchain to the `coverage`
package in your {obj}`pip.parse` hub:

```starlark
filegroup(
name = "coverage",
srcs = ["coverage_main.py"],
data = ["coverage_lib1.py", ...]
python.single_version_platform_override(
coverage_tool = "@pypi//coverage",
platform = "x86_64-unknown-linux-gnu",
python_version = "3.13.2",
)
```

Using `filegroup` isn't required, nor are including client libraries. The
important behaviors of the target are:
Or set {attr}`py_runtime.coverage_tool` to `@pypi//coverage` when defining
your own runtime.

* It provides a single output file OR it provides an executable output; this
output is treated as the coverage entry point.
* If it provides runfiles, then `runfiles.files` are included into `py_test`.
:::{note}
The `py_library` itself depends on the Python toolchain. To avoid a dependency
cycle, the coverage tool is built in a configuration where the toolchain has
no coverage tool, which is similar to how `rules_go` builds `nogo`. This only
works when the runtime is defined with the {obj}`py_runtime` macro, which the
toolchains of `rules_python` use.
:::
2 changes: 1 addition & 1 deletion examples/pip_parse_vendored/requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ all_data_requirements = [

_packages = [
("my_project_pip_deps_vendored_certifi", "certifi==2026.10.23"),
("my_project_pip_deps_vendored_charset_normalizer", "charset-normalizer==3.5.2 --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c"),
("my_project_pip_deps_vendored_charset_normalizer", "charset-normalizer==3.5.2 --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c"),
("my_project_pip_deps_vendored_idna", "idna==3.20"),
("my_project_pip_deps_vendored_requests", "requests==2.34.2"),
("my_project_pip_deps_vendored_urllib3", "urllib3==2.8.0"),
Expand Down
7 changes: 7 additions & 0 deletions news/4415.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(toolchains) With bzlmod, {attr}`python.toolchain.configure_coverage_tool`
uses the `coverage` package from the new `@rules_python_coverage` pip hub, which
locks the latest `coverage` version for each Python version starting from 3.9
and supports all of the platforms configured with {obj}`pip.default`. Passing a
`py_library` as the {attr}`py_runtime.coverage_tool` is now the preferred way,
and using a target without `PyInfo` will be deprecated in the next version
([#3902](https://github.com/bazel-contrib/rules_python/issues/3902)).
4 changes: 4 additions & 0 deletions news/4433.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(toolchains) {attr}`py_runtime.coverage_tool` accepts targets providing
{obj}`PyInfo`, such as `@pypi//coverage`, so the locked version of `coverage`
can be used for coverage collection
([#3902](https://github.com/bazel-contrib/rules_python/issues/3902)).
2 changes: 2 additions & 0 deletions news/4455.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(pypi) {attr}`pip.parse.target_platforms` accepts `"*"` to evaluate the
requirements for all of the platforms configured with {obj}`pip.default`.
5 changes: 5 additions & 0 deletions news/4456.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(pypi) The default {obj}`pip.default` platforms now cover all of the platforms
of the hermetic Python toolchains: `linux_armv7`, `linux_i386`, `linux_ppc`,
`linux_riscv64`, `linux_s390x`, `linux_x86_64_musl`, `osx_arm64e` and
`windows_x86_32`, together with their `_freethreaded` variants. The glibc Linux
platforms now also match on the {flag}`--py_linux_libc` flag.
22 changes: 21 additions & 1 deletion python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "bool_setting")
load("//python:py_binary.bzl", "py_binary")
load("//python:py_library.bzl", "py_library")
load(":bazel_config_mode.bzl", "bazel_config_mode")
Expand Down Expand Up @@ -164,6 +164,25 @@ alias(

bazel_config_mode(name = "bazel_config_mode")

# Usually False. The `py_runtime.coverage_tool` transition sets it to True so
# that the coverage tool and its dependencies (e.g. a `py_library` from a pip hub)
# resolve to a toolchain without a coverage tool, which would otherwise be a
# dependency cycle.
bool_setting(
name = "bootstrap_coverage_tool",
build_setting_default = False,
visibility = ["//visibility:private"],
)

config_setting(
name = "coverage_tool_active",
flag_values = {
":bootstrap_coverage_tool": "False",
},
# This is only because it is an implicit dependency by the toolchains.
visibility = NOT_ACTUALLY_PUBLIC,
)

# This should only be set by analysis tests to expose additional metadata to
# aid testing, so a setting instead of a flag.
bool_flag(
Expand Down Expand Up @@ -689,6 +708,7 @@ bzl_library(
deps = [
":common_labels",
":flags",
":py_info",
":py_internal",
":py_runtime_info",
":reexports",
Expand Down
1 change: 1 addition & 0 deletions python/private/common_labels.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
labels = struct(
# keep sorted
ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")),
BOOTSTRAP_COVERAGE_TOOL = str(Label("//python/private:bootstrap_coverage_tool")),
BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")),
BUILD_PYTHON_ZIP = str(Label("//python/config_settings:build_python_zip")),
# NOTE: Special target; see definition for details.
Expand Down
21 changes: 21 additions & 0 deletions python/private/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
load("//python/uv:lock.bzl", "lock")

package(default_visibility = ["//:__subpackages__"])

exports_files(["uv.lock"])

distribution_filegroup(
name = "distribution",
)

# Run `bazel run //python/private/coverage:requirements.update` to update the
# `uv.lock` file.
lock(
name = "requirements",
srcs = ["pyproject.toml"],
out = "uv.lock",
args = [
"--upgrade", # always upgrade
],
)
22 changes: 22 additions & 0 deletions python/private/coverage/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# The coverage tool used by default for the hermetic toolchains when
# `python.toolchain(configure_coverage_tool = True)` is used.
#
# Newer coverage versions drop support for the older python versions, so there
# is a dependency for each python version so that the latest supported version
# is locked for each of them.
#
# Run `bazel run //python/private/coverage:requirements.update` to update the
# `uv.lock` file.
[project]
name = "rules_python_coverage"
version = "0.0.0"
requires-python = ">=3.9"
dependencies = [
"coverage; python_full_version < '3.10'",
"coverage; python_full_version >= '3.10' and python_full_version < '3.11'",
"coverage; python_full_version >= '3.11' and python_full_version < '3.12'",
"coverage; python_full_version >= '3.12' and python_full_version < '3.13'",
"coverage; python_full_version >= '3.13' and python_full_version < '3.14'",
"coverage; python_full_version >= '3.14' and python_full_version < '3.15'",
"coverage; python_full_version >= '3.15'",
]
Loading
Loading