diff --git a/examples/bzlmod/.coveragerc b/examples/bzlmod/.coveragerc new file mode 100644 index 0000000000..f1ddf67ddd --- /dev/null +++ b/examples/bzlmod/.coveragerc @@ -0,0 +1,8 @@ +[report] +include_namespace_packages=True +skip_covered = True +[run] +relative_files = True +branch = True +omit = + */external/* diff --git a/examples/bzlmod/BUILD.bazel b/examples/bzlmod/BUILD.bazel index d684b9c31d..1133c989b9 100644 --- a/examples/bzlmod/BUILD.bazel +++ b/examples/bzlmod/BUILD.bazel @@ -82,3 +82,5 @@ build_test( name = "all_requirements", targets = all_requirements, ) + +exports_files([".coveragerc"]) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index b7b46b7dba..0f35af17f7 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -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", diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock index 8d02256e95..abaaa8342a 100644 --- a/examples/bzlmod/MODULE.bazel.lock +++ b/examples/bzlmod/MODULE.bazel.lock @@ -1231,7 +1231,7 @@ }, "@@rules_python~//python/extensions:pip.bzl%pip": { "general": { - "bzlTransitiveDigest": "9hiLCuWaaaU7Q+l2ONVr1A0NcG1JfSihv1UYeA1SpNY=", + "bzlTransitiveDigest": "vZDYVzQe/nmLs8d7HoTNqvQYLHaGHh0XSebKRnIDpP0=", "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=", "recordedFileInputs": { "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314", @@ -6140,7 +6140,7 @@ }, "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": { "general": { - "bzlTransitiveDigest": "VoK/T0JkBdcomCHnDIYkX+stkywdxrh1MVM16e8D4sE=", + "bzlTransitiveDigest": "sIrzq5YVq5R5TmhLk3Xl6U9hVZij2dWdP03oI7ZwFaU=", "usagesDigest": "Y8ihY+R57BAFhalrVLVdJFrpwlbsiKz9JPJ99ljF7HA=", "recordedFileInputs": { "@@rules_python~//tools/publish/requirements.txt": "031e35d03dde03ae6305fe4b3d1f58ad7bdad857379752deede0f93649991b8a", diff --git a/python/private/common/providers.bzl b/python/private/common/providers.bzl index eb8b910a2e..5ec08bb748 100644 --- a/python/private/common/providers.bzl +++ b/python/private/common/providers.bzl @@ -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, @@ -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, @@ -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 diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl index d1cbea978c..8001cda4b0 100644 --- a/python/private/common/py_executable.bzl +++ b/python/private/common/py_executable.bzl @@ -313,6 +313,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() diff --git a/python/private/common/py_executable_bazel.bzl b/python/private/common/py_executable_bazel.bzl index a0cfebad8a..2e3f391b53 100644 --- a/python/private/common/py_executable_bazel.bzl +++ b/python/private/common/py_executable_bazel.bzl @@ -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 @@ -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), @@ -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()) diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl index e0b5fb2313..7fe4c01ac6 100644 --- a/python/private/common/py_runtime_rule.bzl +++ b/python/private/common/py_runtime_rule.bzl @@ -76,6 +76,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 @@ -112,6 +116,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, @@ -218,6 +223,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 = """ diff --git a/python/private/python.bzl b/python/private/python.bzl index 6a265d1395..78b6c5b193 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -35,6 +35,7 @@ def _python_register_toolchains(name, toolchain_attr, module, ignore_root_user_e name = name, python_version = toolchain_attr.python_version, register_coverage_tool = toolchain_attr.configure_coverage_tool, + coverage_rc = toolchain_attr.coverage_rc, ignore_root_user_error = ignore_root_user_error, ) return struct( @@ -287,6 +288,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(): @@ -343,6 +345,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 = """\ diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 0f9c90b3b3..2942f5f603 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -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) diff --git a/python/private/python_repositories.bzl b/python/private/python_repositories.bzl index 25d8a96b79..2878f4a4fc 100644 --- a/python/private/python_repositories.bzl +++ b/python/private/python_repositories.bzl @@ -398,6 +398,7 @@ py_runtime( name = "py3_runtime", files = [":files"], {coverage_attr} + coverage_rc = "{coverage_rc}", interpreter = "{python_path}", interpreter_version_info = {{ "major": "{interpreter_version_info_major}", @@ -433,6 +434,7 @@ py_exec_tools_toolchain( python_version = python_short_version, python_version_nodot = python_short_version.replace(".", ""), coverage_attr = coverage_attr_text, + coverage_rc = rctx.attr.coverage_rc, interpreter_version_info_major = python_version_info[0], interpreter_version_info_minor = python_version_info[1], interpreter_version_info_micro = python_version_info[2], @@ -445,6 +447,7 @@ py_exec_tools_toolchain( 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, @@ -496,6 +499,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. " + @@ -567,6 +576,7 @@ def python_register_toolchains( distutils_content = None, register_toolchains = True, register_coverage_tool = False, + coverage_rc = None, set_python_version_constraint = False, tool_versions = TOOL_VERSIONS, **kwargs): @@ -660,6 +670,7 @@ def python_register_toolchains( distutils_content = distutils_content, strip_prefix = strip_prefix, coverage_tool = coverage_tool, + coverage_rc = coverage_rc, **kwargs ) if register_toolchains: diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index f66c28bd51..fa6fc40ee4 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -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 ===== @@ -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,