diff --git a/.bazelrc b/.bazelrc index b484751c3c..1ca469cd75 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -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/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,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_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered -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/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,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_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered +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_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,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_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered +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_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,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_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index bd94003bb7..cef9f7746f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ A brief description of the categories of changes: * (gazelle): Update error messages when unable to resolve a dependency to be more human-friendly. * (flags) The {obj}`--python_version` flag now also returns {obj}`config_common.FeatureFlagInfo`. +* (toolchain): The toolchain patches now expose the `patch_strip` attribute + that one should use when patching toolchains. Please set it if you are + patching python interpreter. In the next release the default will be set to + `0` which better reflects the defaults used in public `bazel` APIs. ### Fixed * (whl_library): Remove `--no-index` and add `--no-build-isolation` to the @@ -51,6 +55,10 @@ A brief description of the categories of changes: ### Added +* (bzlmod): Toolchain overrides can now be done using the new + {bzl:obj}`python.override`, {bzl:obj}`python.single_version_override` and + {bzl:obj}`python.single_version_platform_override` tag classes. + See [#2081](https://github.com/bazelbuild/rules_python/issues/2081). * (rules) Executables provide {obj}`PyExecutableInfo`, which contains executable-specific information useful for packaging an executable or or deriving a new one from the original. diff --git a/MODULE.bazel b/MODULE.bazel index 9ac3e7a04c..100eae8569 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -81,7 +81,7 @@ dev_python = use_extension( "python", dev_dependency = True, ) -dev_python.rules_python_private_testing( +dev_python.override( register_all_versions = True, ) diff --git a/docs/toolchains.md b/docs/toolchains.md index fac1bfc6b0..c31585079e 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -164,6 +164,17 @@ Remember to call `use_repo()` to make repos visible to your module: Python toolchains can be utilized in other bazel rules, such as `genrule()`, by adding the `toolchains=["@rules_python//python:current_py_toolchain"]` attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the {gh-path}`test_current_py_toolchain ` target for an example. +### Overriding toolchain defaults and adding more toolchains + +One can perform various overrides for the registered toolchains from the root module. For example, the following usecases would be supported using the existing attributes: + +* Limiting the available toolchains for the entire `bzlmod` transitive graph + via {attr}`python.override.available_python_versions`. +* Setting particular `X.Y.Z` python versions when modules request `X.Y` version + via {attr}`python.override.minor_mapping`. +* Adding custom {attr}`python.single_version_platform_override.coverage_tool`. +* Adding new python versions via {bzl:obj}`python.single_version_override` or + {bzl:obj}`python.single_version_platform_override`. ## Workspace configuration @@ -240,5 +251,5 @@ automatically registers a higher-priority toolchain; it won't be used unless there is a toolchain misconfiguration somewhere. To aid migration off the Bazel-builtin toolchain, rules_python provides -{obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent +{bzl:obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent toolchain, but is implemented using rules_python's objects. diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index b7b46b7dba..502c6fa712 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -22,7 +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, - # Only set when you have mulitple toolchain versions. + # Only set when you have multiple toolchain versions. is_default = True, python_version = "3.9", ) @@ -37,6 +37,54 @@ python.toolchain( python_version = "3.10", ) +# One can override the actual toolchain versions that are available, which can be useful +# to when optimizing what gets downloaded and when. +python.override( + available_python_versions = [ + "3.10.9", + "3.9.19", + # The following is used by the `other_module` and we need to include it here + # as well. + "3.11.8", + ], + # Also override the `minor_mapping` so that when the modules specify a particular + # `3.X` version, we decide what gets used. + minor_mapping = { + "3.10": "3.10.9", + "3.11": "3.11.8", + "3.9": "3.9.19", + }, +) + +# Or the sources that the toolchains come from for all platforms +python.single_version_override( + patch_strip = 1, + # The user can specify patches to be applied to all interpreters. + patches = [], + python_version = "3.10.2", + sha256 = { + "aarch64-apple-darwin": "1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef", + "aarch64-unknown-linux-gnu": "8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703", + "x86_64-apple-darwin": "8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a", + "x86_64-pc-windows-msvc": "a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd", + "x86_64-unknown-linux-gnu": "9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd", + }, + urls = ["20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz"], +) + +# Or a single platform. This can be used in combination with the +# `single_version_override` and `single_version_platform_override` will be +# applied after `single_version_override`. Any values present in this override +# will overwrite the values set by the `single_version_override` +python.single_version_platform_override( + patch_strip = 1, + patches = [], + platform = "aarch64-apple-darwin", + python_version = "3.10.2", + sha256 = "1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef", + urls = ["20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz"], +) + # You only need to load this repositories if you are using multiple Python versions. # See the tests folder for various examples on using multiple Python versions. # The names "python_3_9" and "python_3_10" are autmatically created by the repo diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock index df0c96d6ca..d4dd7fc02f 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": "NoDlhLqt5TG5tTcsLeBN6mnecYE+w8R4pvOKOF0ctC4=", + "bzlTransitiveDigest": "oVdgglMbVVTKi8i59RPwHvlFpa5JUUQUQNY+Hd71x5o=", "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": "sh8kec0fTFOgrFmLOXLvQRfq+2g3Uv7rF0gj5xqnzKQ=", + "bzlTransitiveDigest": "qSZddiDGXTcAX4lPkTG0vUZaaHaQJBuP8r5WHu/bYgo=", "usagesDigest": "Y8ihY+R57BAFhalrVLVdJFrpwlbsiKz9JPJ99ljF7HA=", "recordedFileInputs": { "@@rules_python~//tools/publish/requirements.txt": "031e35d03dde03ae6305fe4b3d1f58ad7bdad857379752deede0f93649991b8a", diff --git a/python/extensions/python.bzl b/python/extensions/python.bzl index 4148d90877..ce98dc3fec 100644 --- a/python/extensions/python.bzl +++ b/python/extensions/python.bzl @@ -12,7 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Python toolchain module extensions for use with bzlmod" +"""Python toolchain module extensions for use with bzlmod. + +## Basic usage + +The simplest way to configure the toolchain with `rules_python` is as follows. + +```starlark +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.11", +) +use_repo(python, "python_3_11") +``` + +For more in-depth documentation see the {rule}`python.toolchain`. + +## Overrides + +Overrides can be done at 3 different levels: +* Overrides affecting all python toolchain versions on all platforms - {obj}`python.override`. +* Overrides affecting a single toolchain versions on all platforms - {obj}`python.single_version_override`. +* Overrides affecting a single toolchain versions on a single platforms - {obj}`python.single_version_platform_override`. + +:::{seealso} +The main documentation page on registering [toolchains](/toolchains). +::: +""" load("//python/private:python.bzl", _python = "python") diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 252606c0ff..7cbd057c0c 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -131,11 +131,13 @@ bzl_library( name = "python_bzl", srcs = ["python.bzl"], deps = [ + ":full_version_bzl", + ":python_repositories_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", + ":semver_bzl", ":toolchains_repo_bzl", ":util_bzl", - "//python:repositories_bzl", "@bazel_features//:features", ], ) @@ -161,7 +163,6 @@ bzl_library( name = "pythons_hub_bzl", srcs = ["pythons_hub.bzl"], deps = [ - ":full_version_bzl", ":py_toolchain_suite_bzl", "//python:versions_bzl", ], @@ -294,6 +295,12 @@ bzl_library( srcs = ["repo_utils.bzl"], ) +bzl_library( + name = "semver_bzl", + srcs = ["semver.bzl"], + visibility = ["//:__subpackages__"], +) + bzl_library( name = "stamp_bzl", srcs = ["stamp.bzl"], diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl index e0b5fb2313..1cc8361e54 100644 --- a/python/private/common/py_runtime_rule.bzl +++ b/python/private/common/py_runtime_rule.bzl @@ -204,8 +204,8 @@ See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables. "coverage_tool": attr.label( allow_files = False, doc = """ -This is a target to use for collecting code coverage information from `py_binary` -and `py_test` targets. +This is a target to use for collecting code coverage information from +{rule}`py_binary` and {rule}`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, @@ -214,7 +214,7 @@ 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 +of [`coverage.py`](https://coverage.readthedocs.io), at least including the `run` and `lcov` subcommands. """, ), @@ -306,7 +306,7 @@ The template to use when two stage bootstrapping is enabled default = DEFAULT_STUB_SHEBANG, doc = """ "Shebang" expression prepended to the bootstrapping Python stub script -used when executing `py_binary` targets. +used when executing {rule}`py_binary` targets. See https://github.com/bazelbuild/bazel/issues/8685 for motivation. diff --git a/python/private/full_version.bzl b/python/private/full_version.bzl index 98eeee59a1..5377d7f4f5 100644 --- a/python/private/full_version.bzl +++ b/python/private/full_version.bzl @@ -16,18 +16,19 @@ load("//python:versions.bzl", "MINOR_MAPPING") -def full_version(version): +def full_version(version, minor_mapping = MINOR_MAPPING): """Return a full version. Args: version: the version in `X.Y` or `X.Y.Z` format. + minor_mapping: the mapping from `X.Y` to `X.Y.Z`. Returns: a full version given the version string. If the string is already a major version then we return it as is. """ - if version in MINOR_MAPPING: - return MINOR_MAPPING[version] + if version in minor_mapping: + return minor_mapping[version] parts = version.split(".") if len(parts) == 3: @@ -36,7 +37,7 @@ def full_version(version): fail( "Unknown Python version '{}', available values are: {}".format( version, - ",".join(MINOR_MAPPING.keys()), + ",".join(minor_mapping.keys()), ), ) else: diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 3b11dbe7f8..65bdc4a011 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -70,6 +70,7 @@ bzl_library( "//python/private:full_version_bzl", "//python/private:normalize_name_bzl", "//python/private:version_label_bzl", + "//python/private:semver_bzl", "@bazel_features//:features", ] + [ "@pythons_hub//:interpreters_bzl", diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 1bc8f15149..77a477899e 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -19,6 +19,7 @@ load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_L load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/private:semver.bzl", "semver") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") load(":evaluate_markers.bzl", "evaluate_markers", EVALUATE_MARKERS_SRCS = "SRCS") @@ -32,22 +33,8 @@ load(":simpleapi_download.bzl", "simpleapi_download") load(":whl_library.bzl", "whl_library") load(":whl_repo_name.bzl", "whl_repo_name") -def _parse_version(version): - major, _, version = version.partition(".") - minor, _, version = version.partition(".") - patch, _, version = version.partition(".") - build, _, version = version.partition(".") - - return struct( - # use semver vocabulary here - major = major, - minor = minor, - patch = patch, # this is called `micro` in the Python interpreter versioning scheme - build = build, - ) - def _major_minor_version(version): - version = _parse_version(version) + version = semver(version) return "{}.{}".format(version.major, version.minor) def _whl_mods_impl(mctx): diff --git a/python/private/python.bzl b/python/private/python.bzl index 6a265d1395..f47308773d 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -12,13 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Python toolchain module extensions for use with bzlmod" +"Python toolchain module extensions for use with bzlmod." load("@bazel_features//:features.bzl", "bazel_features") -load("//python:repositories.bzl", "python_register_toolchains") -load("//python:versions.bzl", "TOOL_VERSIONS") -load("//python/private:repo_utils.bzl", "repo_utils") +load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") +load(":auth.bzl", "AUTH_ATTRS") +load(":full_version.bzl", "full_version") +load(":python_repositories.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") +load(":repo_utils.bzl", "repo_utils") +load(":semver.bzl", "semver") load(":text_util.bzl", "render") load(":toolchains_repo.bzl", "multi_toolchain_aliases") load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") @@ -28,57 +31,53 @@ load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") _MAX_NUM_TOOLCHAINS = 9999 _TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS)) -def _python_register_toolchains(name, toolchain_attr, module, ignore_root_user_error): - """Calls python_register_toolchains and returns a struct used to collect the toolchains. - """ - python_register_toolchains( - name = name, - python_version = toolchain_attr.python_version, - register_coverage_tool = toolchain_attr.configure_coverage_tool, - ignore_root_user_error = ignore_root_user_error, - ) - return struct( - python_version = toolchain_attr.python_version, - name = name, - module = struct(name = module.name, is_root = module.is_root), - ) +def parse_mods(*, mctx, logger, debug = False, fail = fail): + """parse_mods returns a struct with parsed tag class content. -def _python_impl(module_ctx): - if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": - debug_info = { - "toolchains_registered": [], - } - else: - debug_info = None + Args: + mctx: {type}`module_ctx`. + logger: logger for diagnostic output. + debug: whether to add extra diagnostic information about the configured toolchains. + fail: {type}`function` the fail for failure handling. + + Returns: + a struct with attributes + """ # The toolchain_info structs to register, in the order to register them in. # NOTE: The last element is special: it is treated as the default toolchain, # so there is special handling to ensure the last entry is the correct one. toolchains = [] - # We store the default toolchain separately to ensure it is the last - # toolchain added to toolchains. - # This is a toolchain_info struct. - default_toolchain = None - - # Map of string Major.Minor to the toolchain_info struct + # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct global_toolchain_versions = {} ignore_root_user_error = None - logger = repo_utils.logger(module_ctx, "python") + # We store the default toolchain separately to ensure it is the last + # toolchain added to toolchains. + # This is a toolchain_info struct. + default_toolchain = None # if the root module does not register any toolchain then the # ignore_root_user_error takes its default value: False - if not module_ctx.modules[0].tags.toolchain: + if not mctx.modules[0].tags.toolchain: ignore_root_user_error = False - for mod in module_ctx.modules: - module_toolchain_versions = [] + seen_versions = {} - toolchain_attr_structs = _create_toolchain_attr_structs(mod) + # overrides that can be changed by the root module + overrides = _process_overrides(modules = mctx.modules) - for toolchain_attr in toolchain_attr_structs: + for mod in mctx.modules: + module_toolchain_versions = [] + requested_toolchains = _process_tag_classes( + mod, + seen_versions = seen_versions, + overrides = overrides, + ) + + for toolchain_attr in requested_toolchains: toolchain_version = toolchain_attr.python_version toolchain_name = "python_" + toolchain_version.replace(".", "_") @@ -139,18 +138,13 @@ def _python_impl(module_ctx): ) toolchain_info = None else: - toolchain_info = _python_register_toolchains( - toolchain_name, - toolchain_attr, - module = mod, - ignore_root_user_error = ignore_root_user_error, + toolchain_info = struct( + python_version = toolchain_version, + name = toolchain_name, + module = struct(name = mod.name, is_root = mod.is_root), + register_coverage_tool = toolchain_attr.configure_coverage_tool, ) global_toolchain_versions[toolchain_version] = toolchain_info - if debug_info: - debug_info["toolchains_registered"].append({ - "ignore_root_user_error": ignore_root_user_error, - "name": toolchain_name, - }) if is_default: # This toolchain is setting the default, but the actual @@ -166,6 +160,8 @@ def _python_impl(module_ctx): elif toolchain_info: toolchains.append(toolchain_info) + overrides.default.setdefault("ignore_root_user_error", ignore_root_user_error) + # A default toolchain is required so that the non-version-specific rules # are able to match a toolchain. if default_toolchain == None: @@ -184,23 +180,79 @@ def _python_impl(module_ctx): if len(toolchains) > _MAX_NUM_TOOLCHAINS: fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS)) + return struct( + default_python_version = default_toolchain.python_version, + toolchains = [ + struct( + name = t.name, + python_version = t.python_version, + register_coverage_tool = t.register_coverage_tool, + ) if not debug else struct( + name = t.name, + python_version = t.python_version, + register_coverage_tool = t.register_coverage_tool, + debug = { + "ignore_root_user_error": ignore_root_user_error, + "module": t.module, + } if debug else None, + ) + for t in toolchains + ], + overrides = overrides, + ) + +def _python_impl(mctx): + logger = repo_utils.logger(mctx, "python") + + if mctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": + debug_info = { + "toolchains_registered": [], + } + else: + debug_info = None + + py = parse_mods(mctx = mctx, logger = logger, debug = debug_info != None) + + for toolchain in py.toolchains: + # Ensure that we pass the full version here. + full_python_version = full_version(toolchain.python_version, py.overrides.minor_mapping) + kwargs = { + "python_version": full_python_version, + "register_coverage_tool": toolchain.register_coverage_tool, + } + + # Allow overrides per python version + kwargs.update(py.overrides.kwargs.get(toolchain.python_version, {})) + kwargs.update(py.overrides.kwargs.get(full_python_version, {})) + kwargs.update(py.overrides.default) + python_register_toolchains(name = toolchain.name, **kwargs) + if debug_info: + debug_info["default"] = py.overrides.default + debug_info["toolchains_registered"].append(dict( + name = toolchain.name, + **toolchain.debug + )) + # Create the pythons_hub repo for the interpreter meta data and the # the various toolchains. hub_repo( name = "pythons_hub", - default_python_version = default_toolchain.python_version, + default_python_version = py.default_python_version, toolchain_prefixes = [ render.toolchain_prefix(index, toolchain.name, _TOOLCHAIN_INDEX_PAD_LENGTH) - for index, toolchain in enumerate(toolchains) + for index, toolchain in enumerate(py.toolchains) + ], + toolchain_python_versions = [ + full_version(t.python_version, py.overrides.minor_mapping) + for t in py.toolchains ], - toolchain_python_versions = [t.python_version for t in toolchains], # The last toolchain is the default; it can't have version constraints # Despite the implication of the arg name, the values are strs, not bools toolchain_set_python_version_constraints = [ - "True" if i != len(toolchains) - 1 else "False" - for i in range(len(toolchains)) + "True" if i != len(py.toolchains) - 1 else "False" + for i in range(len(py.toolchains)) ], - toolchain_user_repository_names = [t.name for t in toolchains], + toolchain_user_repository_names = [t.name for t in py.toolchains], ) # This is require in order to support multiple version py_test @@ -208,8 +260,8 @@ def _python_impl(module_ctx): multi_toolchain_aliases( name = "python_versions", python_versions = { - version: toolchain.name - for version, toolchain in global_toolchain_versions.items() + toolchain.python_version: toolchain.name + for toolchain in py.toolchains }, ) @@ -220,7 +272,7 @@ def _python_impl(module_ctx): ) if bazel_features.external_deps.extension_metadata_has_reproducible: - return module_ctx.extension_metadata(reproducible = True) + return mctx.extension_metadata(reproducible = True) else: return None @@ -232,6 +284,9 @@ def _fail_duplicate_module_toolchain_version(version, module): )) def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_name, second_module_name, logger): + if not logger: + return + logger.info(lambda: ( "Ignoring toolchain '{second_toolchain}' from module '{second_module}': " + "Toolchain '{first_toolchain}' from module '{first_module}' " + @@ -252,26 +307,193 @@ def _fail_multiple_default_toolchains(first, second): second = second, )) -def _create_toolchain_attr_structs(mod): - arg_structs = [] - seen_versions = {} - for tag in mod.tags.toolchain: - arg_structs.append(_create_toolchain_attrs_struct(tag = tag, toolchain_tag_count = len(mod.tags.toolchain))) - seen_versions[tag.python_version] = True +def _process_overrides(*, modules, fail = fail): + kwargs = {} + minor_mapping = {} + default = {} + base_url = DEFAULT_RELEASE_BASE_URL + available_versions = { + version: { + # Use a dicts straight away so that we could do URL overrides for a + # single version. + "sha256": dict(item["sha256"]), + "strip_prefix": { + platform: item["strip_prefix"] + for platform in item["sha256"] + }, + "url": { + platform: [item["url"]] + for platform in item["sha256"] + }, + } + for version, item in TOOL_VERSIONS.items() + } + register_all = False + + for mod in modules: + if not mod.is_root: + break + + overriden_keys = [] + for tag in mod.tags.single_version_override: + key = (tag.python_version,) + if key not in overriden_keys: + overriden_keys.append(key) + else: + fail("Only a single python.single_override can be present for each 'python_version', observed a duplicate override for '{}'".format( + *key + )) + + if tag.sha256 or tag.urls: + if not (tag.sha256 and tag.urls): + fail("Both `sha256` and `urls` overrides need to be provided together") + + for platform in tag.sha256 or []: + if platform not in PLATFORMS: + fail("The platform must be one of {allowed} but got '{got}'".format( + allowed = sorted(PLATFORMS), + got = platform, + )) + + sha256 = dict(tag.sha256) or available_versions[tag.python_version]["sha256"] + override = { + "sha256": sha256, + "strip_prefix": { + platform: tag.strip_prefix + for platform in sha256 + }, + "url": { + platform: list(tag.urls) + for platform in tag.sha256 + } or available_versions[tag.python_version]["url"], + } + + if tag.patches: + override["patch_strip"] = { + platform: tag.patch_strip + for platform in sha256 + } + override["patches"] = { + platform: list(tag.patches) + for platform in sha256 + } + + available_versions[tag.python_version] = {k: v for k, v in override.items() if v} + + if tag.distutils_content: + kwargs.setdefault(tag.python_version, {})["distutils_content"] = tag.distutils_content + if tag.distutils: + kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils + + for tag in mod.tags.single_version_platform_override: + key = (tag.python_version, tag.platform) + if key not in overriden_keys: + overriden_keys.append(key) + else: + fail("Only a single python.single_version_platform_override can be present for each ('python_version', 'platform') tuple, duplicate override for ('{}', '{}')".format( + *key + )) + + if tag.python_version not in available_versions: + if not tag.urls or not tag.sha256 or not tag.strip_prefix: + fail("When introducing a new python_version '{}', 'sha256', 'strip_prefix' and 'urls' must be specified".format(tag.python_version)) + available_versions[tag.python_version] = {} + + if tag.coverage_tool: + available_versions[tag.python_version].setdefault("coverage_tool", {})[tag.platform] = tag.coverage_tool + if tag.patch_strip: + available_versions[tag.python_version].setdefault("patch_strip", {})[tag.platform] = tag.patch_strip + if tag.patches: + available_versions[tag.python_version].setdefault("patches", {})[tag.platform] = list(tag.patches) + if tag.sha256: + available_versions[tag.python_version].setdefault("sha256", {})[tag.platform] = tag.sha256 + if tag.strip_prefix: + available_versions[tag.python_version].setdefault("strip_prefix", {})[tag.platform] = tag.strip_prefix + if tag.urls: + available_versions[tag.python_version].setdefault("url", {})[tag.platform] = tag.urls + + for tag in mod.tags.override: + key = ("",) + if key not in overriden_keys: + overriden_keys.append(overriden_keys) + else: + fail("Only a single 'python.override' can be present") + + base_url = tag.base_url + if tag.available_python_versions: + all_versions = dict(available_versions) + available_versions.clear() + available_versions.update({ + v: all_versions[v] if v in all_versions else fail("unknown version '{}', known versions are: {}".format( + v, + sorted(all_versions), + )) + for v in tag.available_python_versions + }) - if mod.is_root: - register_all = False - for tag in mod.tags.rules_python_private_testing: if tag.register_all_versions: register_all = True - break - if register_all: - arg_structs.extend([ - _create_toolchain_attrs_struct(python_version = v) - for v in TOOL_VERSIONS.keys() - if v not in seen_versions - ]) - return arg_structs + + if tag.minor_mapping: + for minor_version, full_version in tag.minor_mapping.items(): + parsed = semver(minor_version) + if parsed.patch or parsed.build: + fail("Expected the key to be of `X.Y` format but got `{}`".format(minor_version)) + parsed = semver(full_version) + if not parsed.patch: + fail("Expected the value to at least be of `X.Y.Z` format but got `{}`".format(minor_version)) + + minor_mapping = tag.minor_mapping + + for key in sorted(AUTH_ATTRS) + ["ignore_root_user_error"]: + if getattr(tag, key, None): + default[key] = getattr(tag, key) + + if not minor_mapping: + versions = {} + for version_string in available_versions: + v = semver(version_string) + versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string)) + + minor_mapping = { + major_minor: max(subset)[1] + for major_minor, subset in versions.items() + } + + default.update({ + "base_url": base_url, + "tool_versions": available_versions, + }) + return struct( + kwargs = kwargs, + minor_mapping = minor_mapping, + default = default, + register_all = register_all, + ) + +def _process_tag_classes(mod, *, overrides, seen_versions): + registrations = [] + + for tag in mod.tags.toolchain: + registrations.append(_create_toolchain_attrs_struct( + tag = tag, + toolchain_tag_count = len(mod.tags.toolchain), + )) + + seen_versions[tag.python_version] = True + + if not mod.is_root: + return registrations + + if overrides.register_all: + # FIXME @aignas 2024-08-30: this is technically not correct + registrations.extend([ + _create_toolchain_attrs_struct(python_version = v) + for v in overrides.default["tool_versions"].keys() + overrides.minor_mapping.keys() + if v not in seen_versions + ]) + + return registrations def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolchain_tag_count = None): if tag and python_version: @@ -297,78 +519,298 @@ def _get_bazel_version_specific_kwargs(): return kwargs -python = module_extension( - doc = """Bzlmod extension that is used to register Python toolchains. -""", - implementation = _python_impl, - tag_classes = { - "rules_python_private_testing": tag_class( - attrs = { - "register_all_versions": attr.bool(default = False), - }, - ), - "toolchain": tag_class( - doc = """Tag class used to register Python toolchains. +_toolchain = tag_class( + doc = """Tag class used to register Python toolchains. Use this tag class to register one or more Python toolchains. This class is also potentially called by sub modules. The following covers different business rules and use cases. -Toolchains in the Root Module +:::{topic} Toolchains in the Root Module This class registers all toolchains in the root module. +::: -Toolchains in Sub Modules +:::{topic} Toolchains in Sub Modules It will create a toolchain that is in a sub module, if the toolchain of the same name does not exist in the root module. The extension stops name clashing between toolchains in the root module and toolchains in sub modules. You cannot configure more than one toolchain as the default toolchain. +::: -Toolchain set as the default version +:::{topic} Toolchain set as the default version This extension will not create a toolchain that exists in a sub module, if the sub module toolchain is marked as the default version. If you have more than one toolchain in your root module, you need to set one of the toolchains as the default version. If there is only one toolchain it is set as the default toolchain. +::: -Toolchain repository name +:::{topic} Toolchain repository name A toolchain's repository name uses the format `python_{major}_{minor}`, e.g. `python_3_10`. The `major` and `minor` components are `major` and `minor` are the Python version from the `python_version` attribute. + +If a toolchain is registered in `X.Y.Z`, then similarly the toolchain name will +be `python_{major}_{minor}_{patch}`, e.g. `python_3_10_19`. +::: + +:::{topic} Toolchain detection +The definition of the first toolchain wins, which means that the root module +can override settings for any python toolchain available. This relies on the +documented module traversal from the {obj}`module_ctx.modules`. +::: + +:::{tip} +In order to use a different name than the above, you can use the following `MODULE.bazel` +syntax: +```starlark +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.11", +) + +use_repo(python, my_python_name = "python_3_11") +``` + +Then the python interpreter will be available as `my_python_name`. +::: """, - attrs = { - "configure_coverage_tool": attr.bool( - mandatory = False, - doc = "Whether or not to configure the default coverage tool for the toolchains.", - ), - "ignore_root_user_error": attr.bool( - default = False, - doc = """\ -If False, the Python runtime installation will be made read only. This improves + attrs = { + "configure_coverage_tool": attr.bool( + mandatory = False, + doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains.", + ), + "ignore_root_user_error": attr.bool( + default = False, + doc = """\ +If `False`, the Python runtime installation will be made read only. This improves the ability for Bazel to cache it, but prevents the interpreter from creating -pyc files for the standard library dynamically at runtime as they are loaded. +`.pyc` files for the standard library dynamically at runtime as they are loaded. -If True, the Python runtime installation is read-write. This allows the -interpreter to create pyc files for the standard library, but, because they are +If `True`, the Python runtime installation is read-write. This allows the +interpreter to create `.pyc` files for the standard library, but, because they are created as needed, it adversely affects Bazel's ability to cache the runtime and can result in spurious build failures. """, - mandatory = False, - ), - "is_default": attr.bool( - mandatory = False, - doc = "Whether the toolchain is the default version", - ), - "python_version": attr.string( - mandatory = True, - doc = "The Python version, in `major.minor` format, e.g " + - "'3.12', to create a toolchain for. Patch level " + - "granularity (e.g. '3.12.1') is not supported.", - ), - }, + mandatory = False, + ), + "is_default": attr.bool( + mandatory = False, + doc = "Whether the toolchain is the default version", ), + "python_version": attr.string( + mandatory = True, + doc = """\ +The Python version, in `major.minor` or `major.minor.patch` format, e.g +`3.12` (or `3.12.3`), to create a toolchain for. +""", + ), + }, +) + +_override = tag_class( + doc = """Tag class used to override defaults and behaviour of the module extension. + +:::{versionadded} 0.36.0 +::: +""", + attrs = dict( + { + "available_python_versions": attr.string_list( + mandatory = False, + doc = """\ +The list of available python tool versions to use. Must be in `X.Y.Z` format. +If the unknown version given the processing of the extension will fail - all of +the versions in the list have to be defined with +{obj}`python.single_version_override` or +{obj}`python.single_version_platform_override` before they are used in this +list. + +This attribute is usually used in order to ensure that no unexpected transitive +dependencies are introduced. +""", + ), + "base_url": attr.string( + mandatory = False, + doc = "The base URL to be used when downloading toolchains.", + default = DEFAULT_RELEASE_BASE_URL, + ), + "ignore_root_user_error": attr.bool( + default = False, + doc = """\ +If `False`, the Python runtime installation will be made read only. This improves +the ability for Bazel to cache it, but prevents the interpreter from creating +`.pyc` files for the standard library dynamically at runtime as they are loaded. + +If `True`, the Python runtime installation is read-write. This allows the +interpreter to create `.pyc` files for the standard library, but, because they are +created as needed, it adversely affects Bazel's ability to cache the runtime and +can result in spurious build failures. +""", + mandatory = False, + ), + "minor_mapping": attr.string_dict( + mandatory = False, + doc = """\ +The mapping between `X.Y` to `X.Y.Z` versions to be used when setting up +toolchains. It defaults to the interpreter with the highest available patch +version for each minor version. For example if one registers `3.10.3`, `3.10.4` +and `3.11.4` then the default for the `minor_mapping` dict will be: +```starlark +{ + "3.10": "3.10.4", + "3.11": "3.11.4", +} +``` +""", + default = {}, + ), + "register_all_versions": attr.bool(default = False, doc = "Add all versions"), + }, + **AUTH_ATTRS + ), +) + +_single_version_override = tag_class( + doc = """Override single python version URLs and patches for all platforms. + +:::{note} +This will replace any existing configuration for the given python version. +::: + +:::{tip} +If you would like to modify the configuration for a specific `(version, +platform)`, please use the {obj}`single_version_platform_override` tag +class. +::: + +:::{versionadded} 0.36.0 +::: +""", + attrs = { + # NOTE @aignas 2024-09-01: all of the attributes except for `version` + # can be part of the `python.toolchain` call. That would make it more + # ergonomic to define new toolchains and to override values for old + # toolchains. The same semantics of the `first one wins` would apply, + # so technically there is no need for any overrides? + # + # Although these attributes would override the code that is used by the + # code in non-root modules, so technically this could be thought as + # being overridden. + # + # rules_go has a single download call: + # https://github.com/bazelbuild/rules_go/blob/master/go/private/extensions.bzl#L38 + # + # However, we need to understand how to accommodate the fact that + # {attr}`single_version_override.version` only allows patch versions. + "distutils": attr.label( + allow_single_file = True, + doc = "A distutils.cfg file to be included in the Python installation. " + + "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", + mandatory = False, + ), + "distutils_content": attr.string( + doc = "A distutils.cfg file content to be included in the Python installation. " + + "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", + mandatory = False, + ), + "patch_strip": attr.int( + mandatory = False, + doc = "Same as the --strip argument of Unix patch.", + default = 0, + ), + "patches": attr.label_list( + mandatory = False, + doc = "A list of labels pointing to patch files to apply for the interpreter repository. They are applied in the list order and are applied before any platform-specific patches are applied.", + ), + "python_version": attr.string( + mandatory = True, + doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", + ), + "sha256": attr.string_dict( + mandatory = False, + doc = "The python platform to sha256 dict. See {attr}`python.single_version_platform_override.platform` for allowed key values.", + ), + "strip_prefix": attr.string( + mandatory = False, + doc = "The 'strip_prefix' for the archive, defaults to 'python'.", + default = "python", + ), + "urls": attr.string_list( + mandatory = False, + doc = "The URL template to fetch releases for this Python version. See {attr}`python.single_version_platform_override.urls` for documentation.", + ), + }, +) + +_single_version_platform_override = tag_class( + doc = """Override single python version for a single existing platform. + +If the `(version, platform)` is new, we will add it to the existing versions and will +use the same `url` template. + +:::{tip} +If you would like to add or remove platforms to a single python version toolchain +configuration, please use {obj}`single_version_override`. +::: + +:::{versionadded} 0.36.0 +::: +""", + attrs = { + "coverage_tool": attr.label( + doc = """\ +The coverage tool to be used for a particular Python interpreter. This can override +`rules_python` defaults. +""", + ), + "patch_strip": attr.int( + mandatory = False, + doc = "Same as the --strip argument of Unix patch.", + default = 0, + ), + "patches": attr.label_list( + mandatory = False, + doc = "A list of labels pointing to patch files to apply for the interpreter repository. They are applied in the list order and are applied after the common patches are applied.", + ), + "platform": attr.string( + mandatory = True, + values = PLATFORMS.keys(), + doc = "The platform to override the values for, must be one of:\n{}.".format("\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS]))), + ), + "python_version": attr.string( + mandatory = True, + doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", + ), + "sha256": attr.string( + mandatory = False, + doc = "The sha256 for the archive", + ), + "strip_prefix": attr.string( + mandatory = False, + doc = "The 'strip_prefix' for the archive, defaults to 'python'.", + default = "python", + ), + "urls": attr.string_list( + mandatory = False, + doc = "The URL template to fetch releases for this Python version. If the URL template results in a relative fragment, default base URL is going to be used. Occurrences of `{python_version}`, `{platform}` and `{build}` will be interpolated based on the contents in the override and the known {attr}`platform` values.", + ), + }, +) + +python = module_extension( + doc = """Bzlmod extension that is used to register Python toolchains. +""", + implementation = _python_impl, + tag_classes = { + "override": _override, + "single_version_override": _single_version_override, + "single_version_platform_override": _single_version_platform_override, + "toolchain": _toolchain, }, **_get_bazel_version_specific_kwargs() ) diff --git a/python/private/python_repositories.bzl b/python/private/python_repositories.bzl index 8675399699..a6c53b64f8 100644 --- a/python/private/python_repositories.bzl +++ b/python/private/python_repositories.bzl @@ -47,8 +47,10 @@ def http_archive(**kwargs): def py_repositories(): """Runtime dependencies that users must install. - This function should be loaded and called in the user's WORKSPACE. - With bzlmod enabled, this function is not needed since MODULE.bazel handles transitive deps. + This function should be loaded and called in the user's `WORKSPACE`. + + With `bzlmod` enabled, this function is not needed since `MODULE.bazel` + handles transitive deps. """ maybe( internal_config_repo, @@ -178,8 +180,7 @@ def _python_repository_impl(rctx): patches = rctx.attr.patches if patches: for patch in patches: - # Should take the strip as an attr, but this is fine for the moment - rctx.patch(patch, strip = 1) + rctx.patch(patch, strip = rctx.attr.patch_strip) # Write distutils.cfg to the Python installation. if "windows" in platform: @@ -450,6 +451,7 @@ py_exec_tools_toolchain( "ignore_root_user_error": rctx.attr.ignore_root_user_error, "name": rctx.attr.name, "netrc": rctx.attr.netrc, + "patch_strip": rctx.attr.patch_strip, "patches": rctx.attr.patches, "platform": platform, "python_version": python_version, @@ -473,27 +475,11 @@ python_repository = repository_rule( doc = "Override mapping of hostnames to authorization patterns; mirrors the eponymous attribute from http_archive", ), "coverage_tool": attr.string( - # Mirrors the definition at - # https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/python/py_runtime_rule.bzl 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. +This is a target to use for collecting code coverage information from {rule}`py_binary` +and {rule}`py_test` targets. -The target is accepted as a string by the python_repository and evaluated within -the context of the toolchain repository. - -For more information see the official bazel docs -(https://bazel.build/reference/be/python#py_runtime.coverage_tool). +For more information see {attr}`py_runtime.coverage_tool`. """, ), "distutils": attr.label( @@ -515,6 +501,21 @@ For more information see the official bazel docs "netrc": attr.string( doc = ".netrc file to use for authentication; mirrors the eponymous attribute from http_archive", ), + "patch_strip": attr.int( + doc = """Same as the --strip argument of Unix patch. + +:::{note} +The default value of `1` is kept for backwards compatibility, it will be set to +`0` in a later release. +::: + +:::{versionadded} 0.36.0 +::: +""", + # TODO @aignas 2024-08-26: switch to 0 when 0.36.0 is released + default = 1, + mandatory = False, + ), "patches": attr.label_list( doc = "A list of patch files to apply to the unpacked interpreter", mandatory = False, @@ -568,7 +569,7 @@ def python_register_toolchains( set_python_version_constraint = False, tool_versions = None, **kwargs): - """Convenience macro for users which does typical setup. + """Convenience macro for users which does typical setup in `WORKSPACE`. - Create a repository for each built-in platform like "python_3_8_linux_amd64" - this repository is lazily fetched when Python is needed for that platform. @@ -579,6 +580,10 @@ def python_register_toolchains( Users can avoid this macro and do these steps themselves, if they want more control. + With `bzlmod` enabled, this function is not needed since `rules_python` is + handling everything. In order to override the default behaviour from the + root module one can see the docs for the {rule}`python` extension. + Args: name: {type}`str` base name for all created repos, e.g. "python_3_8". python_version: {type}`str` the Python version. @@ -627,7 +632,7 @@ def python_register_toolchains( continue loaded_platforms.append(platform) - (release_filename, urls, strip_prefix, patches) = get_release_info(platform, python_version, base_url, tool_versions) + (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info(platform, python_version, base_url, tool_versions) # allow passing in a tool version coverage_tool = None @@ -652,6 +657,7 @@ def python_register_toolchains( platform = platform, ), sha256 = sha256, + patch_strip = patch_strip, patches = patches, platform = platform, python_version = python_version, diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index 7a8c874ed8..d919d05c4f 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -14,7 +14,6 @@ "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels" -load("//python/private:full_version.bzl", "full_version") load( "//python/private:toolchains_repo.bzl", "python_toolchain_build_file_content", @@ -59,7 +58,7 @@ def _hub_build_file_content( [ python_toolchain_build_file_content( prefix = prefixes[i], - python_version = full_version(python_versions[i]), + python_version = python_versions[i], set_python_version_constraint = set_python_version_constraints[i], user_repository_name = user_repository_names[i], ) diff --git a/python/private/semver.bzl b/python/private/semver.bzl new file mode 100644 index 0000000000..3f495a75f0 --- /dev/null +++ b/python/private/semver.bzl @@ -0,0 +1,36 @@ +# Copyright 2024 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. + +"A semver version parser" + +def semver(version): + """Parse the semver version and return the values as a struct. + + Args: + version: {type}`str` the version string + + Returns: + A {type}`struct` with `major`, `minor`, `patch` and `build` attributes. + """ + major, _, version = version.partition(".") + minor, _, version = version.partition(".") + patch, _, build = version.partition("+") + + return struct( + # use semver vocabulary here + major = major, + minor = minor, + patch = patch, # this is called `micro` in the Python interpreter versioning scheme + build = build, + ) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index df16fb8cf7..c4343caad0 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -56,9 +56,6 @@ def python_toolchain_build_file_content( build_content: Text containing toolchain definitions """ - # We create a list of toolchain content from iterating over - # the enumeration of PLATFORMS. We enumerate PLATFORMS in - # order to get us an index to increment the increment. return "\n\n".join([ """\ py_toolchain_suite( diff --git a/python/versions.bzl b/python/versions.bzl index 2cf9b39e96..1898359f0c 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -636,7 +636,7 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U tool_versions: A dict listing the interpreter versions, their SHAs and URL Returns: - A tuple of (filename, url, and archive strip prefix) + A tuple of (filename, url, archive strip prefix, patches, patch_strip) """ url = tool_versions[python_version]["url"] @@ -669,12 +669,13 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U patches = tool_versions[python_version].get("patches", []) if type(patches) == type({}): - if platform in patches.keys(): - patches = patches[platform] - else: - patches = [] + patches = patches.get(platform, []) + + patch_strip = tool_versions[python_version].get("patch_strip", None) + if type(patch_strip) == type({}): + patch_strip = patch_strip[platform] - return (release_filename, rendered_urls, strip_prefix, patches) + return (release_filename, rendered_urls, strip_prefix, patches, patch_strip) def print_toolchains_checksums(name): native.genrule( @@ -716,5 +717,6 @@ def gen_python_config_settings(name = ""): for platform in PLATFORMS.keys(): native.config_setting( name = "{name}{platform}".format(name = name, platform = platform), + flag_values = PLATFORMS[platform].flag_values, constraint_values = PLATFORMS[platform].compatible_with, ) diff --git a/tests/integration/ignore_root_user_error/bzlmod_test.py b/tests/integration/ignore_root_user_error/bzlmod_test.py index 98715b32ec..1283415987 100644 --- a/tests/integration/ignore_root_user_error/bzlmod_test.py +++ b/tests/integration/ignore_root_user_error/bzlmod_test.py @@ -28,8 +28,16 @@ def test_toolchains(self): debug_info = json.loads(debug_path.read_bytes()) expected = [ - {"ignore_root_user_error": True, "name": "python_3_11"}, - {"ignore_root_user_error": True, "name": "python_3_10"}, + { + "ignore_root_user_error": True, + "module": {"is_root": False, "name": "submodule"}, + "name": "python_3_10", + }, + { + "ignore_root_user_error": True, + "module": {"is_root": True, "name": "ignore_root_user_error"}, + "name": "python_3_11", + }, ] self.assertCountEqual(debug_info["toolchains_registered"], expected) diff --git a/tests/python/BUILD.bazel b/tests/python/BUILD.bazel new file mode 100644 index 0000000000..df2b18101d --- /dev/null +++ b/tests/python/BUILD.bazel @@ -0,0 +1,3 @@ +load(":python_tests.bzl", "python_test_suite") + +python_test_suite(name = "python_tests") diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl new file mode 100644 index 0000000000..15ce755969 --- /dev/null +++ b/tests/python/python_tests.bzl @@ -0,0 +1,627 @@ +# Copyright 2024 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. + +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python:versions.bzl", "MINOR_MAPPING") +load("//python/private:python.bzl", _parse_mods = "parse_mods") # buildifier: disable=bzl-visibility + +_tests = [] + +def parse_mods(*, mctx, **kwargs): + return _parse_mods(mctx = mctx, logger = None, **kwargs) + +def _mock_mctx(*modules, environ = {}): + return struct( + os = struct(environ = environ), + modules = [ + struct( + name = modules[0].name, + tags = modules[0].tags, + is_root = modules[0].is_root, + ), + ] + [ + struct( + name = mod.name, + tags = mod.tags, + is_root = False, + ) + for mod in modules[1:] + ], + ) + +def _mod(*, name, toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = True): + return struct( + name = name, + tags = struct( + toolchain = toolchain, + override = override, + single_version_override = single_version_override, + single_version_platform_override = single_version_platform_override, + ), + is_root = is_root, + ) + +def _toolchain(python_version, *, is_default = False, **kwargs): + return struct( + is_default = is_default, + python_version = python_version, + **kwargs + ) + +def _override( + auth_patterns = {}, + available_python_versions = [], + base_url = "", + ignore_root_user_error = False, + minor_mapping = {}, + netrc = "", + register_all_versions = False): + return struct( + auth_patterns = auth_patterns, + available_python_versions = available_python_versions, + base_url = base_url, + ignore_root_user_error = ignore_root_user_error, + minor_mapping = minor_mapping, + netrc = netrc, + register_all_versions = register_all_versions, + ) + +def _single_version_override( + python_version = "", + sha256 = {}, + urls = [], + patch_strip = 0, + patches = [], + strip_prefix = "python", + distutils_content = "", + distutils = None): + if not python_version: + fail("missing mandatory args: python_version ({})".format(python_version)) + + return struct( + python_version = python_version, + sha256 = sha256, + urls = urls, + patch_strip = patch_strip, + patches = patches, + strip_prefix = strip_prefix, + distutils_content = distutils_content, + distutils = distutils, + ) + +def _single_version_platform_override( + coverage_tool = None, + patch_strip = 0, + patches = [], + platform = "", + python_version = "", + sha256 = "", + strip_prefix = "python", + urls = []): + if not platform or not python_version: + fail("missing mandatory args: platform ({}) and python_version ({})".format(platform, python_version)) + + return struct( + sha256 = sha256, + urls = urls, + strip_prefix = strip_prefix, + platform = platform, + coverage_tool = coverage_tool, + python_version = python_version, + patch_strip = patch_strip, + patches = patches, + ) + +def _test_default(env): + py = parse_mods( + mctx = _mock_mctx( + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + + # The value there should be consistent in bzlmod with the automatically + # calculated value Please update the MINOR_MAPPING in //python:versions.bzl + # when this part starts failing. + env.expect.that_dict(py.overrides.minor_mapping).contains_exactly(MINOR_MAPPING) + env.expect.that_collection(py.overrides.kwargs).has_size(0) + env.expect.that_collection(py.overrides.default.keys()).contains_exactly([ + "base_url", + "ignore_root_user_error", + "tool_versions", + ]) + env.expect.that_bool(py.overrides.default["ignore_root_user_error"]).equals(False) + env.expect.that_str(py.default_python_version).equals("3.11") + + want_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) + +_tests.append(_test_default) + +def _test_default_some_module(env): + py = parse_mods( + mctx = _mock_mctx( + _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), + ), + ) + + # The value there should be consistent in bzlmod with the automatically + # calculated value Please update the MINOR_MAPPING in //python:versions.bzl + # when this part starts failing. + env.expect.that_dict(py.overrides.minor_mapping).contains_exactly(MINOR_MAPPING) + env.expect.that_collection(py.overrides.kwargs).has_size(0) + env.expect.that_collection(py.overrides.default.keys()).contains_exactly([ + "base_url", + "ignore_root_user_error", + "tool_versions", + ]) + env.expect.that_str(py.default_python_version).equals("3.11") + + want_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) + +_tests.append(_test_default_some_module) + +def _test_default_with_patch(env): + py = parse_mods( + mctx = _mock_mctx( + _mod(name = "rules_python", toolchain = [_toolchain("3.11.2")]), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.11.2") + + want_toolchain = struct( + name = "python_3_11_2", + python_version = "3.11.2", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) + +_tests.append(_test_default_with_patch) + +def _test_default_non_rules_python(env): + py = parse_mods( + mctx = _mock_mctx( + # NOTE @aignas 2024-09-06: the first item in the module_ctx.modules + # could be a non-root module, which is the case if the root module + # does not make any calls to the extension. + _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.11") + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([rules_python_toolchain]) + +_tests.append(_test_default_non_rules_python) + +def _test_default_non_rules_python_ignore_root_user_error(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.12", ignore_root_user_error = True)], + ), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + + env.expect.that_bool(py.overrides.default["ignore_root_user_error"]).equals(True) + env.expect.that_str(py.default_python_version).equals("3.12") + + my_module_toolchain = struct( + name = "python_3_12", + python_version = "3.12", + register_coverage_tool = False, + ) + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([ + rules_python_toolchain, + my_module_toolchain, + ]).in_order() + +_tests.append(_test_default_non_rules_python_ignore_root_user_error) + +def _test_default_non_rules_python_ignore_root_user_error_override(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.12")], + override = [_override(ignore_root_user_error = True)], + ), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + + env.expect.that_bool(py.overrides.default["ignore_root_user_error"]).equals(True) + env.expect.that_str(py.default_python_version).equals("3.12") + + my_module_toolchain = struct( + name = "python_3_12", + python_version = "3.12", + register_coverage_tool = False, + ) + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([ + rules_python_toolchain, + my_module_toolchain, + ]).in_order() + +_tests.append(_test_default_non_rules_python_ignore_root_user_error_override) + +def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): + py = parse_mods( + mctx = _mock_mctx( + _mod(name = "my_module", toolchain = [_toolchain("3.13")]), + _mod(name = "some_module", toolchain = [_toolchain("3.12", ignore_root_user_error = True)]), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_bool(py.overrides.default["ignore_root_user_error"]).equals(False) + + my_module_toolchain = struct( + name = "python_3_13", + python_version = "3.13", + register_coverage_tool = False, + ) + some_module_toolchain = struct( + name = "python_3_12", + python_version = "3.12", + register_coverage_tool = False, + ) + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([ + some_module_toolchain, + rules_python_toolchain, + my_module_toolchain, + ]).in_order() + +_tests.append(_test_default_non_rules_python_ignore_root_user_error_non_root_module) + +def _test_first_occurance_of_the_toolchain_wins(env): + py = parse_mods( + mctx = _mock_mctx( + _mod(name = "my_module", toolchain = [_toolchain("3.12")]), + _mod(name = "some_module", toolchain = [_toolchain("3.12", configure_coverage_tool = True)]), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + debug = True, + ) + + env.expect.that_str(py.default_python_version).equals("3.12") + + my_module_toolchain = struct( + name = "python_3_12", + python_version = "3.12", + # NOTE: coverage stays disabled even though `some_module` was + # configuring something else. + register_coverage_tool = False, + debug = { + "ignore_root_user_error": False, + "module": struct(is_root = True, name = "my_module"), + }, + ) + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + debug = { + "ignore_root_user_error": False, + "module": struct(is_root = False, name = "rules_python"), + }, + ) + env.expect.that_collection(py.toolchains).contains_exactly([ + rules_python_toolchain, + my_module_toolchain, # default toolchain is last + ]).in_order() + +_tests.append(_test_first_occurance_of_the_toolchain_wins) + +def _test_auth_overrides(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.12")], + override = [ + _override( + netrc = "/my/netrc", + auth_patterns = {"foo": "bar"}, + ), + ], + ), + _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + ), + ) + + env.expect.that_dict(py.overrides.default).contains_at_least({ + "auth_patterns": {"foo": "bar"}, + "ignore_root_user_error": False, + "netrc": "/my/netrc", + }) + env.expect.that_str(py.default_python_version).equals("3.12") + + my_module_toolchain = struct( + name = "python_3_12", + python_version = "3.12", + register_coverage_tool = False, + ) + rules_python_toolchain = struct( + name = "python_3_11", + python_version = "3.11", + register_coverage_tool = False, + ) + env.expect.that_collection(py.toolchains).contains_exactly([ + rules_python_toolchain, + my_module_toolchain, + ]).in_order() + +_tests.append(_test_auth_overrides) + +def _test_add_new_version(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.13")], + single_version_override = [ + _single_version_override( + python_version = "3.13.0", + sha256 = { + "aarch64-unknown-linux-gnu": "deadbeef", + }, + urls = ["example.org"], + patch_strip = 0, + patches = [], + strip_prefix = "prefix", + distutils_content = "", + distutils = None, + ), + ], + single_version_platform_override = [ + _single_version_platform_override( + sha256 = "deadb00f", + urls = ["something.org", "else.org"], + strip_prefix = "python", + platform = "aarch64-unknown-linux-gnu", + coverage_tool = "specific_cov_tool", + python_version = "3.13.1", + patch_strip = 2, + patches = ["specific-patch.txt"], + ), + ], + override = [ + _override( + base_url = "", + available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], + minor_mapping = { + "3.13": "3.13.0", + }, + ), + ], + ), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_collection(py.overrides.default["tool_versions"].keys()).contains_exactly([ + "3.12.4", + "3.13.0", + "3.13.1", + ]) + env.expect.that_dict(py.overrides.default["tool_versions"]["3.13.0"]).contains_exactly({ + "sha256": {"aarch64-unknown-linux-gnu": "deadbeef"}, + "strip_prefix": {"aarch64-unknown-linux-gnu": "prefix"}, + "url": {"aarch64-unknown-linux-gnu": ["example.org"]}, + }) + env.expect.that_dict(py.overrides.default["tool_versions"]["3.13.1"]).contains_exactly({ + "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"}, + "patch_strip": {"aarch64-unknown-linux-gnu": 2}, + "patches": {"aarch64-unknown-linux-gnu": ["specific-patch.txt"]}, + "sha256": {"aarch64-unknown-linux-gnu": "deadb00f"}, + "strip_prefix": {"aarch64-unknown-linux-gnu": "python"}, + "url": {"aarch64-unknown-linux-gnu": ["something.org", "else.org"]}, + }) + env.expect.that_dict(py.overrides.minor_mapping).contains_exactly({ + "3.13": "3.13.0", + }) + env.expect.that_collection(py.toolchains).contains_exactly([ + struct( + name = "python_3_13", + python_version = "3.13", + register_coverage_tool = False, + ), + ]) + +_tests.append(_test_add_new_version) + +def _test_register_all_versions(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.13")], + single_version_override = [ + _single_version_override( + python_version = "3.13.0", + sha256 = { + "aarch64-unknown-linux-gnu": "deadbeef", + }, + urls = ["example.org"], + ), + ], + single_version_platform_override = [ + _single_version_platform_override( + sha256 = "deadb00f", + urls = ["something.org"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.1", + ), + ], + override = [ + _override( + base_url = "", + available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], + register_all_versions = True, + ), + ], + ), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_collection(py.overrides.default["tool_versions"].keys()).contains_exactly([ + "3.12.4", + "3.13.0", + "3.13.1", + ]) + env.expect.that_dict(py.overrides.minor_mapping).contains_exactly({ + # The mapping is calculated automatically + "3.12": "3.12.4", + "3.13": "3.13.1", + }) + env.expect.that_collection(py.toolchains).contains_exactly([ + struct( + name = name, + python_version = version, + register_coverage_tool = False, + ) + for name, version in { + "python_3_12": "3.12", + "python_3_12_4": "3.12.4", + "python_3_13": "3.13", + "python_3_13_0": "3.13.0", + "python_3_13_1": "3.13.1", + }.items() + ]) + +_tests.append(_test_register_all_versions) + +def _test_add_patches(env): + py = parse_mods( + mctx = _mock_mctx( + _mod( + name = "my_module", + toolchain = [_toolchain("3.13")], + single_version_override = [ + _single_version_override( + python_version = "3.13.0", + sha256 = { + "aarch64-apple-darwin": "deadbeef", + "aarch64-unknown-linux-gnu": "deadbeef", + }, + urls = ["example.org"], + patch_strip = 1, + patches = ["common.txt"], + strip_prefix = "prefix", + distutils_content = "", + distutils = None, + ), + ], + single_version_platform_override = [ + _single_version_platform_override( + sha256 = "deadb00f", + urls = ["something.org", "else.org"], + strip_prefix = "python", + platform = "aarch64-unknown-linux-gnu", + coverage_tool = "specific_cov_tool", + python_version = "3.13.0", + patch_strip = 2, + patches = ["specific-patch.txt"], + ), + ], + override = [ + _override( + base_url = "", + available_python_versions = ["3.13.0"], + minor_mapping = { + "3.13": "3.13.0", + }, + ), + ], + ), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_dict(py.overrides.default["tool_versions"]).contains_exactly({ + "3.13.0": { + "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"}, + "patch_strip": {"aarch64-apple-darwin": 1, "aarch64-unknown-linux-gnu": 2}, + "patches": { + "aarch64-apple-darwin": ["common.txt"], + "aarch64-unknown-linux-gnu": ["specific-patch.txt"], + }, + "sha256": {"aarch64-apple-darwin": "deadbeef", "aarch64-unknown-linux-gnu": "deadb00f"}, + "strip_prefix": {"aarch64-apple-darwin": "prefix", "aarch64-unknown-linux-gnu": "python"}, + "url": { + "aarch64-apple-darwin": ["example.org"], + "aarch64-unknown-linux-gnu": ["something.org", "else.org"], + }, + }, + }) + env.expect.that_dict(py.overrides.minor_mapping).contains_exactly({ + "3.13": "3.13.0", + }) + env.expect.that_collection(py.toolchains).contains_exactly([ + struct( + name = "python_3_13", + python_version = "3.13", + register_coverage_tool = False, + ), + ]) + +_tests.append(_test_add_patches) + +# TODO @aignas 2024-09-03: add failure tests: +# * validate the python_version in overrides +# * incorrect platform failure +# * missing python_version failure + +def python_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests)