Skip to content

feat(toolchain) Add coveragepy configuration attribute #2224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions examples/bzlmod/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[report]
include_namespace_packages=True
skip_covered = True
[run]
relative_files = True
branch = True
omit =
*/external/*
2 changes: 2 additions & 0 deletions examples/bzlmod/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,5 @@ build_test(
name = "all_requirements",
targets = all_requirements,
)

exports_files([".coveragerc"])
1 change: 1 addition & 0 deletions examples/bzlmod/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bazel_dep(name = "protobuf", version = "24.4", repo_name = "com_google_protobuf"
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(
configure_coverage_tool = True,
coverage_rc = "//:.coveragerc",
# Only set when you have mulitple toolchain versions.
is_default = True,
python_version = "3.9",
Expand Down
4 changes: 2 additions & 2 deletions examples/bzlmod/MODULE.bazel.lock

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

8 changes: 8 additions & 0 deletions python/private/common/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def _PyRuntimeInfo_init(
interpreter = None,
files = None,
coverage_tool = None,
coverage_rc = None,
coverage_files = None,
pyc_tag = None,
python_version,
Expand Down Expand Up @@ -121,6 +122,7 @@ def _PyRuntimeInfo_init(
"bootstrap_template": bootstrap_template,
"coverage_files": coverage_files,
"coverage_tool": coverage_tool,
"coverage_rc": coverage_rc,
"files": files,
"implementation_name": implementation_name,
"interpreter": interpreter,
Expand Down Expand Up @@ -202,6 +204,12 @@ The files required at runtime for using `coverage_tool`. Will be `None` if no

If set, this field is a `File` representing tool used for collecting code
coverage information from python tests. Otherwise, this is `None`.
""",
"coverage_rc": """
:type: File | None

If set, this field is a `File` representing the configuration file used by the
coverage information from python tests. Otherwise, this is `None`.
""",
"files": """
:type: depset[File] | None
Expand Down
2 changes: 2 additions & 0 deletions python/private/common/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ def _get_runtime_details(ctx, semantics):
direct.append(effective_runtime.coverage_tool)
if effective_runtime.coverage_files:
transitive.append(effective_runtime.coverage_files)
if effective_runtime.coverage_rc:
direct.append(effective_runtime.coverage_rc)
runtime_files = depset(direct = direct, transitive = transitive)
else:
runtime_files = depset()
Expand Down
11 changes: 11 additions & 0 deletions python/private/common/py_executable_bazel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ def _create_stage2_bootstrap(
)
else:
coverage_tool_runfiles_path = ""
if runtime and runtime.coverage_rc:
coverage_rc_path = runtime.coverage_rc.path
else:
coverage_rc_path = ""

template = runtime.stage2_bootstrap_template

Expand All @@ -351,6 +355,7 @@ def _create_stage2_bootstrap(
output = output,
substitutions = {
"%coverage_tool%": coverage_tool_runfiles_path,
"%coverage_rc%": coverage_rc_path,
"%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
"%imports%": ":".join(imports.to_list()),
"%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path),
Expand Down Expand Up @@ -403,6 +408,12 @@ def _create_stage1_bootstrap(
subs["%shebang%"] = DEFAULT_STUB_SHEBANG
template = ctx.file._bootstrap_template

if runtime and runtime.coverage_rc:
coverage_rc_path = runtime.coverage_rc.path
else:
coverage_rc_path = ""

subs["%coverage_rc%"] = coverage_rc_path
subs["%coverage_tool%"] = coverage_tool_runfiles_path
subs["%import_all%"] = ("True" if ctx.fragments.bazel_py.python_import_all_repositories else "False")
subs["%imports%"] = ":".join(imports.to_list())
Expand Down
11 changes: 11 additions & 0 deletions python/private/common/py_runtime_rule.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def _py_runtime_impl(ctx):
else:
coverage_tool = None
coverage_files = None
if ctx.attr.coverage_rc:
coverage_rc = ctx.attr.coverage_rc.files.to_list()[0]
else:
coverage_rc = None

python_version = ctx.attr.python_version

Expand Down Expand Up @@ -117,6 +121,7 @@ def _py_runtime_impl(ctx):
py_runtime_info_kwargs.update(dict(
implementation_name = ctx.attr.implementation_name,
interpreter_version_info = interpreter_version_info,
coverage_rc = coverage_rc,
pyc_tag = pyc_tag,
stage2_bootstrap_template = ctx.file.stage2_bootstrap_template,
zip_main_template = ctx.file.zip_main_template,
Expand Down Expand Up @@ -216,6 +221,12 @@ of coverage.py (https://coverage.readthedocs.io), at least including
the `run` and `lcov` subcommands.
""",
),
"coverage_rc": attr.label(
allow_single_file = True,
doc = ".converage or pyproject.toml or " +
"for configure coverage tool",
mandatory = False,
),
"files": attr.label_list(
allow_files = True,
doc = """
Expand Down
6 changes: 5 additions & 1 deletion python/private/hermetic_runtime_repo_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def define_hermetic_runtime_toolchain_impl(
extra_files_glob_exclude,
python_version,
python_bin,
coverage_tool):
coverage_tool,
coverage_rc):
"""Define a toolchain implementation for a python-build-standalone repo.

It expected this macro is called in the top-level package of an extracted
Expand All @@ -47,6 +48,8 @@ def define_hermetic_runtime_toolchain_impl(
repositoroy.
coverage_tool: {type}`str` optional target to the coverage tool to
use.
coverage_rc: {type}`str` optional target to the coverage rc file to
use.
"""
_ = name # @unused
version_info = semver(python_version)
Expand Down Expand Up @@ -134,6 +137,7 @@ def define_hermetic_runtime_toolchain_impl(
},
# Convert empty string to None
coverage_tool = coverage_tool or None,
coverage_rc = coverage_rc or None,
python_version = "PY3",
implementation_name = "cpython",
# See https://peps.python.org/pep-3147/ for pyc tag infix format
Expand Down
5 changes: 5 additions & 0 deletions python/private/python.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolcha
python_version = python_version if python_version else tag.python_version,
configure_coverage_tool = getattr(tag, "configure_coverage_tool", False),
ignore_root_user_error = getattr(tag, "ignore_root_user_error", False),
coverage_rc = getattr(tag, "coverage_rc", None),
)

def _get_bazel_version_specific_kwargs():
Expand Down Expand Up @@ -375,6 +376,10 @@ A toolchain's repository name uses the format `python_{major}_{minor}`, e.g.
mandatory = False,
doc = "Whether or not to configure the default coverage tool for the toolchains.",
),
"coverage_rc": attr.label(
mandatory = False,
doc = "The coverage configuration file to use for the toolchains.",
),
"ignore_root_user_error": attr.bool(
default = False,
doc = """\
Expand Down
8 changes: 5 additions & 3 deletions python/private/python_bootstrap_template.txt
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,11 @@ def _RunForCoverage(python_program, main_filename, args, env,
"""
# We need for coveragepy to use relative paths. This can only be configured
unique_id = uuid.uuid4()
rcfile_name = os.path.join(os.environ['COVERAGE_DIR'], ".coveragerc_{}".format(unique_id))
with open(rcfile_name, "w") as rcfile:
rcfile.write('''[run]
rcfile_name = "%coverage_rc%"
if not rcfile_name:
rcfile_name = os.path.join(os.environ['COVERAGE_DIR'], ".coveragerc_{}".format(unique_id))
with open(rcfile_name, "w") as rcfile:
rcfile.write('''[run]
relative_files = True
''')
PrintVerboseCoverage('Coverage entrypoint:', coverage_entrypoint)
Expand Down
11 changes: 11 additions & 0 deletions python/private/python_repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,15 @@ define_hermetic_runtime_toolchain_impl(
python_version = {python_version},
python_bin = {python_bin},
coverage_tool = {coverage_tool},
coverage_rc = {coverage_rc},
)
""".format(
extra_files_glob_exclude = render.list(glob_exclude),
extra_files_glob_include = render.list(glob_include),
python_bin = render.str(python_bin),
python_version = render.str(rctx.attr.python_version),
coverage_tool = render.str(coverage_tool),
coverage_rc = rctx.attr.coverage_rc,
)
rctx.delete("python")
rctx.symlink(python_bin, "python")
Expand All @@ -329,6 +331,7 @@ define_hermetic_runtime_toolchain_impl(
attrs = {
"auth_patterns": rctx.attr.auth_patterns,
"coverage_tool": rctx.attr.coverage_tool,
"coverage_rc": rctx.attr.coverage_rc,
"distutils": rctx.attr.distutils,
"distutils_content": rctx.attr.distutils_content,
"ignore_root_user_error": rctx.attr.ignore_root_user_error,
Expand Down Expand Up @@ -381,6 +384,12 @@ For more information see the official bazel docs
(https://bazel.build/reference/be/python#py_runtime.coverage_tool).
""",
),
"coverage_rc": attr.label(
allow_single_file = True,
doc = ".converage or pyproject.toml or " +
"for configure coverage tool",
mandatory = False,
),
"distutils": attr.label(
allow_single_file = True,
doc = "A distutils.cfg file to be included in the Python installation. " +
Expand Down Expand Up @@ -465,6 +474,7 @@ def python_register_toolchains(
python_version,
register_toolchains = True,
register_coverage_tool = False,
coverage_rc = None,
set_python_version_constraint = False,
tool_versions = None,
minor_mapping = None,
Expand Down Expand Up @@ -564,6 +574,7 @@ def python_register_toolchains(
urls = urls,
strip_prefix = strip_prefix,
coverage_tool = coverage_tool,
coverage_rc = coverage_rc,
**kwargs
)
if register_toolchains:
Expand Down
19 changes: 11 additions & 8 deletions python/private/stage2_bootstrap_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
IMPORT_ALL = True if "%import_all%" == "True" else False
# Runfiles-relative path to the coverage tool entry point, if any.
COVERAGE_TOOL = "%coverage_tool%"
COVERAGE_RCFILE = "%coverage_rc%"

# ===== Template substitutions end =====

Expand Down Expand Up @@ -344,14 +345,16 @@ def _maybe_collect_coverage(enable):
coverage_dir = os.environ["COVERAGE_DIR"]
unique_id = uuid.uuid4()

# We need for coveragepy to use relative paths. This can only be configured
rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id))
with open(rcfile_name, "w") as rcfile:
rcfile.write(
"""[run]
relative_files = True
"""
)
rcfile_name = COVERAGE_RCFILE
if not rcfile_name:
# We need for coveragepy to use relative paths. This can only be configured
rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id))
with open(rcfile_name, "w") as rcfile:
rcfile.write(
"""[run]
relative_files = True
"""
)
try:
cov = coverage.Coverage(
config_file=rcfile_name,
Expand Down