From 3bf56294ad727ed561a15886d49fdec7fc557e54 Mon Sep 17 00:00:00 2001 From: "Jan Winkler (jwi)" Date: Tue, 13 Jan 2026 17:30:38 +0100 Subject: [PATCH 1/9] feat: enable pyproject.toml as single source of truth for Python version --- .bazelrc.deleted_packages | 4 + .gitattributes | 1 + .gitignore | 4 +- CHANGELOG.md | 3 + python/private/BUILD.bazel | 17 ++- python/private/pypi/BUILD.bazel | 1 + python/private/pypi/extension.bzl | 42 +++++- python/private/pypi/hub_builder.bzl | 41 +++--- python/private/pyproject_repo.bzl | 64 ++++++++ python/private/pyproject_utils.bzl | 38 +++++ python/private/pyproject_version_extractor.py | 45 ++++++ python/private/python.bzl | 82 ++++++++++- tests/pyproject/BUILD.bazel | 0 .../compile_requirements_test/BUILD.bazel | 14 ++ .../compile_requirements_test/MODULE.bazel | 13 ++ .../compile_requirements_test/pyproject.toml | 8 + .../requirements_lock.txt | 137 ++++++++++++++++++ .../test_compile_requirements.py | 27 ++++ .../pip_integration_test/BUILD.bazel | 9 ++ .../pip_integration_test/MODULE.bazel | 21 +++ .../pip_integration_test/pyproject.toml | 8 + .../requirements_lock.txt | 137 ++++++++++++++++++ .../pip_integration_test/test_pip_works.py | 21 +++ tests/pyproject/priority_test/.python-version | 1 + tests/pyproject/priority_test/BUILD.bazel | 6 + tests/pyproject/priority_test/MODULE.bazel | 16 ++ tests/pyproject/priority_test/pyproject.toml | 4 + .../pyproject/priority_test/test_priority.py | 21 +++ .../python_toolchain_test/BUILD.bazel | 7 + .../python_toolchain_test/MODULE.bazel | 13 ++ .../python_toolchain_test/pyproject.toml | 4 + .../python_toolchain_test/test_version.py | 19 +++ 32 files changed, 799 insertions(+), 29 deletions(-) create mode 100644 python/private/pyproject_repo.bzl create mode 100644 python/private/pyproject_utils.bzl create mode 100644 python/private/pyproject_version_extractor.py create mode 100644 tests/pyproject/BUILD.bazel create mode 100644 tests/pyproject/compile_requirements_test/BUILD.bazel create mode 100644 tests/pyproject/compile_requirements_test/MODULE.bazel create mode 100644 tests/pyproject/compile_requirements_test/pyproject.toml create mode 100644 tests/pyproject/compile_requirements_test/requirements_lock.txt create mode 100644 tests/pyproject/compile_requirements_test/test_compile_requirements.py create mode 100644 tests/pyproject/pip_integration_test/BUILD.bazel create mode 100644 tests/pyproject/pip_integration_test/MODULE.bazel create mode 100644 tests/pyproject/pip_integration_test/pyproject.toml create mode 100644 tests/pyproject/pip_integration_test/requirements_lock.txt create mode 100644 tests/pyproject/pip_integration_test/test_pip_works.py create mode 100644 tests/pyproject/priority_test/.python-version create mode 100644 tests/pyproject/priority_test/BUILD.bazel create mode 100644 tests/pyproject/priority_test/MODULE.bazel create mode 100644 tests/pyproject/priority_test/pyproject.toml create mode 100644 tests/pyproject/priority_test/test_priority.py create mode 100644 tests/pyproject/python_toolchain_test/BUILD.bazel create mode 100644 tests/pyproject/python_toolchain_test/MODULE.bazel create mode 100644 tests/pyproject/python_toolchain_test/pyproject.toml create mode 100644 tests/pyproject/python_toolchain_test/test_version.py diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index 2d8a8075fa..99cd0e3dea 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -45,3 +45,7 @@ common --deleted_packages=tests/modules/other/nspkg_single common --deleted_packages=tests/modules/other/simple_v1 common --deleted_packages=tests/modules/other/simple_v2 common --deleted_packages=tests/modules/other/with_external_data +common --deleted_packages=tests/pyproject/compile_requirements_test +common --deleted_packages=tests/pyproject/pip_integration_test +common --deleted_packages=tests/pyproject/priority_test +common --deleted_packages=tests/pyproject/python_toolchain_test diff --git a/.gitattributes b/.gitattributes index eae260e931..82cbc9c0a8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ python/features.bzl export-subst tools/publish/*.txt linguist-generated=true +requirements_lock.txt linguist-generated=true diff --git a/.gitignore b/.gitignore index fb1b17e466..bd64959c44 100644 --- a/.gitignore +++ b/.gitignore @@ -48,8 +48,10 @@ user.bazelrc # CLion .clwb -# Python cache +# Python artifacts **/__pycache__/ +*.egg +*.egg-info # MODULE.bazel.lock is ignored for now as per recommendation from upstream. # See https://github.com/bazelbuild/bazel/issues/20369 diff --git a/CHANGELOG.md b/CHANGELOG.md index 61951a342d..c874a4974c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ END_UNRELEASED_TEMPLATE ### Fixed * (runfiles) Fixed `CurrentRepository()` raising `ValueError` on Windows. ([#3579](https://github.com/bazel-contrib/rules_python/issues/3579)) +* (pip) Made `config_settings` optional in `pip.default()` when not using platform-specific configurations. * (tests) No more coverage warnings are being printed if there are no sources. ([#2762](https://github.com/bazel-contrib/rules_python/issues/2762)) * (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`. @@ -87,6 +88,8 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-added} ### Added +* (pip,python) Added `pyproject_toml` attribute to `pip.default()` and `python.defaults()` + to read Python version from pyproject.toml `requires-python` field (must be `==X.Y.Z` format). * (binaries/tests) {obj}`--debugger`: allows specifying an extra dependency to add to binaries/tests for custom debuggers. * (binaries/tests) Build information is now included in binaries and tests. diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 70f7f86413..0ef93813ae 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -29,7 +29,10 @@ package( licenses(["notice"]) -exports_files(["runtime_env_toolchain_interpreter.sh"]) +exports_files([ + "runtime_env_toolchain_interpreter.sh", + "pyproject_version_extractor.py", +]) filegroup( name = "distribution", @@ -272,6 +275,8 @@ bzl_library( deps = [ ":full_version_bzl", ":platform_info_bzl", + ":pyproject_repo_bzl", + ":pyproject_utils_bzl", ":python_register_toolchains_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", @@ -752,6 +757,16 @@ bzl_library( ], ) +bzl_library( + name = "pyproject_repo_bzl", + srcs = ["pyproject_repo.bzl"], +) + +bzl_library( + name = "pyproject_utils_bzl", + srcs = ["pyproject_utils.bzl"], +) + # Needed to define bzl_library targets for docgen. (We don't define the # bzl_library target here because it'd give our users a transitive dependency # on Skylib.) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 4fea3684de..cd883f71b8 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -127,6 +127,7 @@ bzl_library( ":whl_library_bzl", "//python/private:auth_bzl", "//python/private:normalize_name_bzl", + "//python/private:pyproject_utils_bzl", "//python/private:repo_utils_bzl", "@bazel_features//:features", "@pythons_hub//:interpreters_bzl", diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 7354a67e67..279bf4c220 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -19,6 +19,7 @@ load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:normalize_name.bzl", "normalize_name") +load("//python/private:pyproject_utils.bzl", "read_pyproject_version") load("//python/private:repo_utils.bzl", "repo_utils") load(":evaluate_markers.bzl", EVALUATE_MARKERS_SRCS = "SRCS") load(":hub_builder.bzl", "hub_builder") @@ -86,12 +87,23 @@ def build_config( """ defaults = { "platforms": {}, + "python_version": None, } for mod in module_ctx.modules: if not (mod.is_root or mod.name == "rules_python"): continue for tag in mod.tags.default: + pyproject_toml = getattr(tag, "pyproject_toml", None) + if pyproject_toml: + pyproject_version = read_pyproject_version( + module_ctx, + pyproject_toml, + logger = None, + ) + if pyproject_version: + defaults["python_version"] = pyproject_version + platform = tag.platform if platform: specific_config = defaults["platforms"].setdefault(platform, {}) @@ -125,6 +137,7 @@ def build_config( return struct( auth_patterns = defaults.get("auth_patterns", {}), netrc = defaults.get("netrc", None), + python_version = defaults.get("python_version", None), platforms = { name: _plat(**values) for name, values in defaults["platforms"].items() @@ -155,6 +168,8 @@ def parse_modules( Returns: A struct with the following attributes: """ + config = build_config(module_ctx = module_ctx, enable_pipstar = enable_pipstar, enable_pipstar_extract = enable_pipstar_extract) + whl_mods = {} for mod in module_ctx.modules: for whl_mod in mod.tags.whl_mods: @@ -186,8 +201,6 @@ You cannot use both the additive_build_content and additive_build_content_file a srcs_exclude_glob = whl_mod.srcs_exclude_glob, ) - config = build_config(module_ctx = module_ctx, enable_pipstar = enable_pipstar, enable_pipstar_extract = enable_pipstar_extract) - # TODO @aignas 2025-06-03: Merge override API with the builder? _overriden_whl_set = {} whl_overrides = {} @@ -228,6 +241,10 @@ You cannot use both the additive_build_content and additive_build_content_file a for mod in module_ctx.modules: for pip_attr in mod.tags.parse: + python_version = pip_attr.python_version or config.python_version + if not python_version: + _fail("pip.parse() requires either python_version attribute or pip.default(pyproject_toml=...) to be set") + hub_name = pip_attr.hub_name if hub_name not in pip_hub_map: builder = hub_builder( @@ -264,6 +281,7 @@ You cannot use both the additive_build_content and additive_build_content_file a builder.pip_parse( module_ctx, pip_attr = pip_attr, + python_version = python_version, ) # Keeps track of all the hub's whl repos across the different versions. @@ -409,7 +427,7 @@ Either this or {attr}`env` `platform_machine` key should be specified. """, ), "config_settings": attr.label_list( - mandatory = True, + mandatory = False, doc = """\ The list of labels to `config_setting` targets that need to be matched for the platform to be selected. @@ -471,6 +489,22 @@ If you are defining custom platforms in your project and don't want things to cl [isolation] feature. [isolation]: https://bazel.build/rules/lib/globals/module#use_extension.isolate +""", + ), + "pyproject_toml": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml and uses +it as the default python_version for all `pip.parse()` calls that don't +explicitly specify one. + +The version must be specified as `==X.Y.Z` (exact version with full semver). +This is designed to work with dependency management tools like Renovate. + +:::{versionadded} 1.8.0 +::: """, ), "whl_abi_tags": attr.string_list( @@ -651,7 +685,7 @@ find in case extra indexes are specified. default = True, ), "python_version": attr.string( - mandatory = True, + mandatory = False, doc = """ The Python version the dependencies are targetting, in Major.Minor format (e.g., "3.11") or patch level granularity (e.g. "3.11.1"). diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index f0aa6a73bc..47bafc454f 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -147,8 +147,8 @@ def _build(self): whl_libraries = self._whl_libraries, ) -def _pip_parse(self, module_ctx, pip_attr): - python_version = pip_attr.python_version +def _pip_parse(self, module_ctx, pip_attr, python_version = None): + python_version = python_version or pip_attr.python_version if python_version in self._platforms: fail(( "Duplicate pip python version '{version}' for hub " + @@ -197,8 +197,9 @@ def _pip_parse(self, module_ctx, pip_attr): self, module_ctx, pip_attr = pip_attr, - enable_pipstar = self._config.enable_pipstar or self._get_index_urls.get(pip_attr.python_version), - enable_pipstar_extract = self._config.enable_pipstar_extract or self._get_index_urls.get(pip_attr.python_version), + python_version = python_version, + enable_pipstar = self._config.enable_pipstar or self._get_index_urls.get(python_version), + enable_pipstar_extract = self._config.enable_pipstar_extract or self._get_index_urls.get(python_version), ) ### end of PUBLIC methods @@ -410,11 +411,11 @@ def _set_get_index_urls(self, pip_attr): ) return True -def _detect_interpreter(self, pip_attr): +def _detect_interpreter(self, pip_attr, python_version): python_interpreter_target = pip_attr.python_interpreter_target if python_interpreter_target == None and not pip_attr.python_interpreter: python_name = "python_{}_host".format( - pip_attr.python_version.replace(".", "_"), + python_version.replace(".", "_"), ) if python_name not in self._available_interpreters: fail(( @@ -424,7 +425,7 @@ def _detect_interpreter(self, pip_attr): "Expected to find {python_name} among registered versions:\n {labels}" ).format( hub_name = self.name, - version = pip_attr.python_version, + version = python_version, python_name = python_name, labels = " \n".join(self._available_interpreters), )) @@ -488,17 +489,17 @@ def _platforms(module_ctx, *, python_version, config, target_platforms): ) return platforms -def _evaluate_markers(self, pip_attr, enable_pipstar): +def _evaluate_markers(self, pip_attr, python_version, enable_pipstar): if self._evaluate_markers_fn: return self._evaluate_markers_fn if enable_pipstar: return lambda _, requirements: evaluate_markers_star( requirements = requirements, - platforms = self._platforms[pip_attr.python_version], + platforms = self._platforms[python_version], ) - interpreter = _detect_interpreter(self, pip_attr) + interpreter = _detect_interpreter(self, pip_attr, python_version) # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either # in the PATH or if specified as a label. We will configure the env @@ -518,7 +519,7 @@ def _evaluate_markers(self, pip_attr, enable_pipstar): module_ctx, requirements = { k: { - p: self._platforms[pip_attr.python_version][p].triple + p: self._platforms[python_version][p].triple for p in plats } for k, plats in requirements.items() @@ -534,6 +535,7 @@ def _create_whl_repos( module_ctx, *, pip_attr, + python_version, enable_pipstar = False, enable_pipstar_extract = False): """create all of the whl repositories @@ -542,11 +544,12 @@ def _create_whl_repos( self: the builder. module_ctx: {type}`module_ctx`. pip_attr: {type}`struct` - the struct that comes from the tag class iteration. + python_version: {type}`str` - the resolved python version for this pip.parse call. enable_pipstar: {type}`bool` - enable the pipstar or not. enable_pipstar_extract: {type}`bool` - enable the pipstar extraction or not. """ logger = self._logger - platforms = self._platforms[pip_attr.python_version] + platforms = self._platforms[python_version] requirements_by_platform = parse_requirements( module_ctx, requirements_by_platform = requirements_files_by_platform( @@ -558,15 +561,15 @@ def _create_whl_repos( extra_pip_args = pip_attr.extra_pip_args, platforms = sorted(platforms), # here we only need keys python_version = full_version( - version = pip_attr.python_version, + version = python_version, minor_mapping = self._minor_mapping, ), logger = logger, ), platforms = platforms, extra_pip_args = pip_attr.extra_pip_args, - get_index_urls = self._get_index_urls.get(pip_attr.python_version), - evaluate_markers = _evaluate_markers(self, pip_attr, enable_pipstar), + get_index_urls = self._get_index_urls.get(python_version), + evaluate_markers = _evaluate_markers(self, pip_attr, python_version, enable_pipstar), logger = logger, ) @@ -588,7 +591,7 @@ def _create_whl_repos( enable_pipstar = enable_pipstar, ) - interpreter = _detect_interpreter(self, pip_attr) + interpreter = _detect_interpreter(self, pip_attr, python_version) for whl in requirements_by_platform: whl_library_args = common_args | _whl_library_args( @@ -602,9 +605,9 @@ def _create_whl_repos( whl_library_args = whl_library_args, download_only = pip_attr.download_only, netrc = self._config.netrc or pip_attr.netrc, - use_downloader = _use_downloader(self, pip_attr.python_version, whl.name), + use_downloader = _use_downloader(self, python_version, whl.name), auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns, - python_version = _major_minor_version(pip_attr.python_version), + python_version = _major_minor_version(python_version), is_multiple_versions = whl.is_multiple_versions, interpreter = interpreter, enable_pipstar = enable_pipstar, @@ -612,7 +615,7 @@ def _create_whl_repos( ) _add_whl_library( self, - python_version = pip_attr.python_version, + python_version = python_version, whl = whl, repo = repo, enable_pipstar = enable_pipstar, diff --git a/python/private/pyproject_repo.bzl b/python/private/pyproject_repo.bzl new file mode 100644 index 0000000000..e62ec2b4e4 --- /dev/null +++ b/python/private/pyproject_repo.bzl @@ -0,0 +1,64 @@ +"""Repository rule to expose Python version from pyproject.toml.""" + +_EXTRACTOR_SCRIPT = Label("//python/private:pyproject_version_extractor.py") + +def _pyproject_version_repo_impl(rctx): + """Create a repository that exports PYTHON_VERSION from pyproject.toml.""" + pyproject_path = rctx.path(rctx.attr.pyproject_toml) + rctx.read(pyproject_path, watch = "yes") + + # Use the shared extractor script + extractor = rctx.path(_EXTRACTOR_SCRIPT) + result = rctx.execute([ + "python3", + str(extractor), + str(pyproject_path), + ]) + + if result.return_code != 0: + fail("Failed to read Python version from pyproject.toml: " + result.stderr) + + version = result.stdout.strip() + + # Create a .bzl file that exports the version + rctx.file("version.bzl", """ +\"\"\"Python version from pyproject.toml. + +This file is automatically generated. Do not edit. +\"\"\" + +PYTHON_VERSION = "{version}" +""".format(version = version)) + + rctx.file("BUILD.bazel", """ +# Automatically generated from pyproject.toml +exports_files(["version.bzl"]) +""") + +pyproject_version_repo = repository_rule( + implementation = _pyproject_version_repo_impl, + attrs = { + "pyproject_toml": attr.label( + mandatory = True, + allow_single_file = True, + doc = "Label pointing to pyproject.toml file.", + ), + }, + doc = """Repository rule that reads Python version from pyproject.toml. + +This rule creates a repository with a `version.bzl` file that exports +`PYTHON_VERSION` constant. This allows BUILD files to import the Python +version without hardcoding it. + +Example: +```python + load("@python_version_from_pyproject//:version.bzl", "PYTHON_VERSION") + + compile_pip_requirements( + name = "requirements", + python_version = PYTHON_VERSION, + requirements_txt = "requirements.txt", + ) +``` +""", +) diff --git a/python/private/pyproject_utils.bzl b/python/private/pyproject_utils.bzl new file mode 100644 index 0000000000..eaf03332e2 --- /dev/null +++ b/python/private/pyproject_utils.bzl @@ -0,0 +1,38 @@ +"""Utilities for reading Python version from pyproject.toml.""" + +_EXTRACTOR_SCRIPT = Label("//python/private:pyproject_version_extractor.py") + +def read_pyproject_version(module_ctx, pyproject_label, logger = None): + """Reads Python version from pyproject.toml if requested. + + Args: + module_ctx: The module_ctx object from the module extension. + pyproject_label: Label pointing to the pyproject.toml file, or None. + logger: Optional logger instance for informational messages. + + Returns: + The Python version string (e.g. "3.13.9") or None if pyproject_label is None. + """ + if not pyproject_label: + return None + + pyproject_path = module_ctx.path(pyproject_label) + module_ctx.read(pyproject_path, watch = "yes") + + # Use the shared extractor script + extractor = module_ctx.path(_EXTRACTOR_SCRIPT) + result = module_ctx.execute([ + "python3", + str(extractor), + str(pyproject_path), + ]) + + if result.return_code != 0: + fail("Failed to read Python version from pyproject.toml: " + result.stderr) + + version = result.stdout.strip() + + if logger: + logger.info(lambda: "Read Python version {} from {}".format(version, pyproject_label)) + + return version diff --git a/python/private/pyproject_version_extractor.py b/python/private/pyproject_version_extractor.py new file mode 100644 index 0000000000..1cfdbd1fbc --- /dev/null +++ b/python/private/pyproject_version_extractor.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Extract Python version from pyproject.toml.""" + +import re +import sys + +try: + import tomllib as toml +except ImportError: + try: + import tomli as toml + except ImportError: + raise SystemExit( + "need tomllib (python >=3.11) or tomli installed on host python" + ) + + +def validate_and_extract(pyproject_path): + """Validate format and extract version.""" + with open(pyproject_path, "rb") as f: + data = toml.load(f) + + version = data["project"]["requires-python"] + + if not version.startswith("=="): + sys.exit(f"requires-python must use '==' for exact version, got: {version}") + + bare_version = version[2:].strip() + + if not re.match(r"^\d+\.\d+\.\d+$", bare_version): + sys.exit(f"requires-python must be in X.Y.Z format, got: {bare_version}") + + return bare_version + + +def main(): + if len(sys.argv) < 2: + sys.exit("Usage: pyproject_version_extractor.py ") + + version = validate_and_extract(sys.argv[1]) + print(version) + + +if __name__ == "__main__": + main() diff --git a/python/private/python.bzl b/python/private/python.bzl index 399743c18d..8b226336e0 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -19,6 +19,8 @@ load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VER load(":auth.bzl", "AUTH_ATTRS") load(":full_version.bzl", "full_version") load(":platform_info.bzl", "platform_info") +load(":pyproject_repo.bzl", "pyproject_version_repo") +load(":pyproject_utils.bzl", "read_pyproject_version") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") @@ -87,6 +89,8 @@ def parse_modules(*, module_ctx, logger, _fail = fail): mod = mod, seen_versions = seen_versions, config = config, + module_ctx = module_ctx, + logger = logger, ) for toolchain_attr in toolchain_attr_structs: @@ -216,6 +220,20 @@ def _python_impl(module_ctx): logger = repo_utils.logger(module_ctx, "python") py = parse_modules(module_ctx = module_ctx, logger = logger) + # Create pyproject version repo if pyproject.toml is used + created_pyproject_repo = False + for mod in module_ctx.modules: + if mod.is_root: + for tag in mod.tags.defaults: + if tag.pyproject_toml: + pyproject_version_repo( + name = "python_version_from_pyproject", + pyproject_toml = tag.pyproject_toml, + ) + created_pyproject_repo = True + break + break + # Host compatible runtime repos # dict[str version, struct] where struct has: # * full_python_version: str @@ -455,7 +473,16 @@ def _python_impl(module_ctx): ) if bazel_features.external_deps.extension_metadata_has_reproducible: - return module_ctx.extension_metadata(reproducible = True) + # Build the list of direct dependencies + root_direct_deps = ["pythons_hub", "python_versions"] + if created_pyproject_repo: + root_direct_deps.append("python_version_from_pyproject") + + return module_ctx.extension_metadata( + root_module_direct_deps = root_direct_deps, + root_module_direct_dev_deps = [], + reproducible = True, + ) else: return None @@ -852,8 +879,15 @@ def _compute_default_python_version(mctx): defaults_attr_structs = _create_defaults_attr_structs(mod = mod) default_python_version_env = None default_python_version_file = None + pyproject_toml_label = None for defaults_attr in defaults_attr_structs: + pyproject_toml_label = _one_or_the_same( + pyproject_toml_label, + defaults_attr.pyproject_toml, + onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), + ) + default_python_version = _one_or_the_same( default_python_version, defaults_attr.python_version, @@ -869,12 +903,22 @@ def _compute_default_python_version(mctx): defaults_attr.python_version_file, onerror = _fail_multiple_defaults_python_version_file, ) - if default_python_version_file: + + # Priority order: pyproject_toml > python_version_file > python_version_env > python_version + if pyproject_toml_label: + pyproject_version = read_pyproject_version( + mctx, + pyproject_toml_label, + logger = None, + ) + if pyproject_version: + default_python_version = pyproject_version + elif default_python_version_file: default_python_version = _one_or_the_same( default_python_version, mctx.read(default_python_version_file, watch = "yes").strip(), ) - if default_python_version_env: + elif default_python_version_env: default_python_version = mctx.getenv( default_python_version_env, default_python_version, @@ -915,11 +959,29 @@ def _create_defaults_attr_struct(*, tag): python_version = getattr(tag, "python_version", None), python_version_env = getattr(tag, "python_version_env", None), python_version_file = getattr(tag, "python_version_file", None), + pyproject_toml = getattr(tag, "pyproject_toml", None), ) -def _create_toolchain_attr_structs(*, mod, config, seen_versions): +def _create_toolchain_attr_structs(*, mod, config, seen_versions, module_ctx, logger): arg_structs = [] + # Check if pyproject_toml was specified in defaults + # If so, register a toolchain for it + for tag in mod.tags.defaults: + pyproject_toml = getattr(tag, "pyproject_toml", None) + if pyproject_toml: + pyproject_version = read_pyproject_version( + module_ctx, + pyproject_toml, + logger, + ) + if pyproject_version and pyproject_version not in seen_versions: + arg_structs.append(_create_toolchain_attrs_struct( + python_version = pyproject_version, + toolchain_tag_count = 1, + )) + seen_versions[pyproject_version] = True + for tag in mod.tags.toolchain: arg_structs.append(_create_toolchain_attrs_struct( tag = tag, @@ -959,6 +1021,18 @@ def _create_toolchain_attrs_struct( _defaults = tag_class( doc = """Tag class to specify the default Python version.""", attrs = { + "pyproject_toml": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml. +The version must be specified as `==X.Y.Z` (exact version with full semver). + +:::{versionadded} 1.8.0 +::: +""", + ), "python_version": attr.string( mandatory = False, doc = """\ diff --git a/tests/pyproject/BUILD.bazel b/tests/pyproject/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pyproject/compile_requirements_test/BUILD.bazel b/tests/pyproject/compile_requirements_test/BUILD.bazel new file mode 100644 index 0000000000..970bcd287f --- /dev/null +++ b/tests/pyproject/compile_requirements_test/BUILD.bazel @@ -0,0 +1,14 @@ +load("@python_version_from_pyproject//:version.bzl", "PYTHON_VERSION") +load("@rules_python//python:defs.bzl", "py_test") +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +compile_pip_requirements( + name = "requirements", + python_version = PYTHON_VERSION, + requirements_txt = "requirements_lock.txt", +) + +py_test( + name = "test_compile_requirements", + srcs = ["test_compile_requirements.py"], +) diff --git a/tests/pyproject/compile_requirements_test/MODULE.bazel b/tests/pyproject/compile_requirements_test/MODULE.bazel new file mode 100644 index 0000000000..bccb07e00e --- /dev/null +++ b/tests/pyproject/compile_requirements_test/MODULE.bazel @@ -0,0 +1,13 @@ +"""Test that pyproject.toml version can be used in compile_pip_requirements.""" + +module(name = "compile_requirements_test") + +bazel_dep(name = "rules_python", version = "") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults(pyproject_toml = "//:pyproject.toml") +use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/compile_requirements_test/pyproject.toml b/tests/pyproject/compile_requirements_test/pyproject.toml new file mode 100644 index 0000000000..6640278aae --- /dev/null +++ b/tests/pyproject/compile_requirements_test/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "compile-requirements-test" +dynamic = ["version"] +requires-python = "==3.11.7" + +dependencies = [ + "requests==2.32.5" +] diff --git a/tests/pyproject/compile_requirements_test/requirements_lock.txt b/tests/pyproject/compile_requirements_test/requirements_lock.txt new file mode 100644 index 0000000000..65cbf853f4 --- /dev/null +++ b/tests/pyproject/compile_requirements_test/requirements_lock.txt @@ -0,0 +1,137 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# bazel run //:requirements.update +# +certifi==2026.1.4 \ + --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ + --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 + # via requests +charset-normalizer==3.4.4 \ + --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ + --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ + --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ + --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ + --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ + --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ + --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ + --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ + --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ + --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ + --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ + --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ + --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ + --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ + --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ + --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ + --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ + --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ + --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ + --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ + --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ + --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ + --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ + --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ + --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ + --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ + --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ + --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ + --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ + --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ + --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ + --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ + --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ + --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ + --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ + --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ + --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ + --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ + --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ + --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ + --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ + --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ + --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ + --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ + --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ + --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ + --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ + --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ + --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ + --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ + --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ + --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ + --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ + --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ + --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ + --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ + --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ + --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ + --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ + --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ + --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ + --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ + --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ + --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ + --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ + --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ + --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ + --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ + --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ + --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ + --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ + --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ + --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ + --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ + --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ + --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ + --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ + --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ + --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ + --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ + --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ + --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ + --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ + --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ + --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ + --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ + --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ + --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ + --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ + --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ + --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ + --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ + --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ + --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ + --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ + --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ + --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ + --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ + --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ + --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ + --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ + --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ + --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ + --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ + --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ + --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ + --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ + --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ + --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ + --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ + --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ + --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ + --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 + # via requests +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via requests +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via compile-requirements-test (pyproject.toml) +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via requests diff --git a/tests/pyproject/compile_requirements_test/test_compile_requirements.py b/tests/pyproject/compile_requirements_test/test_compile_requirements.py new file mode 100644 index 0000000000..4a7924502c --- /dev/null +++ b/tests/pyproject/compile_requirements_test/test_compile_requirements.py @@ -0,0 +1,27 @@ +"""Test that compile_pip_requirements works with PYTHON_VERSION from pyproject.toml.""" + +import sys +import unittest + + +class CompileRequirementsTest(unittest.TestCase): + def test_python_version_matches_pyproject(self): + """Verify we're using the Python version from pyproject.toml.""" + version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + self.assertEqual( + version, + "3.11.7", + f"Expected Python 3.11.7 from pyproject.toml, got {version}", + ) + + def test_requirements_target_exists(self): + """ + This test just needs to run successfully. + The fact that it runs means compile_pip_requirements worked, + which means PYTHON_VERSION was successfully loaded from the repo. + """ + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pyproject/pip_integration_test/BUILD.bazel b/tests/pyproject/pip_integration_test/BUILD.bazel new file mode 100644 index 0000000000..215e0fb910 --- /dev/null +++ b/tests/pyproject/pip_integration_test/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "test_pip_works", + srcs = ["test_pip_works.py"], + deps = [ + "@pip//requests", + ], +) diff --git a/tests/pyproject/pip_integration_test/MODULE.bazel b/tests/pyproject/pip_integration_test/MODULE.bazel new file mode 100644 index 0000000000..c39ba1d18a --- /dev/null +++ b/tests/pyproject/pip_integration_test/MODULE.bazel @@ -0,0 +1,21 @@ +"""Test that pyproject.toml sets pip python_version.""" + +module(name = "pip_integration_test") + +bazel_dep(name = "rules_python", version = "") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults(pyproject_toml = "//:pyproject.toml") +use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.default(pyproject_toml = "//:pyproject.toml") +pip.parse( + hub_name = "pip", + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "pip") diff --git a/tests/pyproject/pip_integration_test/pyproject.toml b/tests/pyproject/pip_integration_test/pyproject.toml new file mode 100644 index 0000000000..543898e95f --- /dev/null +++ b/tests/pyproject/pip_integration_test/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "pip-integration-test" +dynamic = ["version"] +requires-python = "==3.11.7" + +dependencies = [ + "requests==2.32.5" +] diff --git a/tests/pyproject/pip_integration_test/requirements_lock.txt b/tests/pyproject/pip_integration_test/requirements_lock.txt new file mode 100644 index 0000000000..fa0204bbea --- /dev/null +++ b/tests/pyproject/pip_integration_test/requirements_lock.txt @@ -0,0 +1,137 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --generate-hashes --output-file=requirements_lock.txt pyproject.toml +# +certifi==2026.1.4 \ + --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ + --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 + # via requests +charset-normalizer==3.4.4 \ + --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ + --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ + --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ + --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ + --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ + --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ + --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ + --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ + --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ + --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ + --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ + --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ + --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ + --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ + --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ + --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ + --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ + --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ + --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ + --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ + --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ + --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ + --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ + --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ + --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ + --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ + --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ + --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ + --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ + --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ + --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ + --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ + --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ + --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ + --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ + --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ + --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ + --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ + --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ + --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ + --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ + --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ + --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ + --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ + --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ + --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ + --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ + --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ + --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ + --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ + --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ + --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ + --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ + --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ + --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ + --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ + --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ + --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ + --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ + --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ + --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ + --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ + --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ + --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ + --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ + --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ + --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ + --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ + --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ + --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ + --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ + --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ + --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ + --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ + --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ + --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ + --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ + --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ + --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ + --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ + --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ + --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ + --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ + --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ + --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ + --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ + --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ + --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ + --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ + --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ + --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ + --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ + --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ + --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ + --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ + --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ + --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ + --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ + --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ + --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ + --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ + --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ + --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ + --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ + --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ + --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ + --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ + --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ + --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ + --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ + --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ + --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ + --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 + # via requests +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via requests +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via compile-requirements-test (pyproject.toml) +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via requests diff --git a/tests/pyproject/pip_integration_test/test_pip_works.py b/tests/pyproject/pip_integration_test/test_pip_works.py new file mode 100644 index 0000000000..63d55c87ca --- /dev/null +++ b/tests/pyproject/pip_integration_test/test_pip_works.py @@ -0,0 +1,21 @@ +"""Test that pip dependencies resolve correctly with pyproject.toml.""" + +import sys +import unittest + + +class PipIntegrationTest(unittest.TestCase): + def test_python_version(self): + """Verify Python version from pyproject.toml.""" + version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + self.assertEqual(version, "3.11.7") + + def test_can_import_dependency(self): + """Verify pip dependency was resolved.""" + import requests + + self.assertTrue(callable(requests.get)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pyproject/priority_test/.python-version b/tests/pyproject/priority_test/.python-version new file mode 100644 index 0000000000..655ff07381 --- /dev/null +++ b/tests/pyproject/priority_test/.python-version @@ -0,0 +1 @@ +3.13.9 diff --git a/tests/pyproject/priority_test/BUILD.bazel b/tests/pyproject/priority_test/BUILD.bazel new file mode 100644 index 0000000000..56685c1686 --- /dev/null +++ b/tests/pyproject/priority_test/BUILD.bazel @@ -0,0 +1,6 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "test_priority", + srcs = ["test_priority.py"], +) diff --git a/tests/pyproject/priority_test/MODULE.bazel b/tests/pyproject/priority_test/MODULE.bazel new file mode 100644 index 0000000000..8754c77f37 --- /dev/null +++ b/tests/pyproject/priority_test/MODULE.bazel @@ -0,0 +1,16 @@ +"""Test that pyproject.toml takes priority over other version sources.""" + +module(name = "priority_test") + +bazel_dep(name = "rules_python", version = "") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + pyproject_toml = "//:pyproject.toml", + python_version_file = "//:.python-version", +) +use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/priority_test/pyproject.toml b/tests/pyproject/priority_test/pyproject.toml new file mode 100644 index 0000000000..8268f6f5d7 --- /dev/null +++ b/tests/pyproject/priority_test/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "priority-test" +dynamic = ["version"] +requires-python = "==3.11.7" diff --git a/tests/pyproject/priority_test/test_priority.py b/tests/pyproject/priority_test/test_priority.py new file mode 100644 index 0000000000..2dff7e858c --- /dev/null +++ b/tests/pyproject/priority_test/test_priority.py @@ -0,0 +1,21 @@ +"""Test that pyproject.toml takes priority over .python-version.""" + +import sys +import unittest + + +class PriorityTest(unittest.TestCase): + def test_pyproject_wins_over_python_version_file(self): + """ + Verify pyproject.toml (3.11.7) takes priority over .python-version (3.12.0). + """ + version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + self.assertEqual( + version, + "3.11.7", + "pyproject.toml should take priority over .python-version file", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pyproject/python_toolchain_test/BUILD.bazel b/tests/pyproject/python_toolchain_test/BUILD.bazel new file mode 100644 index 0000000000..5dd2afcbf3 --- /dev/null +++ b/tests/pyproject/python_toolchain_test/BUILD.bazel @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "test_version", + srcs = ["test_version.py"], + main = "test_version.py", +) diff --git a/tests/pyproject/python_toolchain_test/MODULE.bazel b/tests/pyproject/python_toolchain_test/MODULE.bazel new file mode 100644 index 0000000000..bcf9fc2069 --- /dev/null +++ b/tests/pyproject/python_toolchain_test/MODULE.bazel @@ -0,0 +1,13 @@ +"""Test that pyproject.toml sets Python toolchain version.""" + +module(name = "python_toolchain_test") + +bazel_dep(name = "rules_python", version = "") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults(pyproject_toml = "//:pyproject.toml") +use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/python_toolchain_test/pyproject.toml b/tests/pyproject/python_toolchain_test/pyproject.toml new file mode 100644 index 0000000000..aea4119799 --- /dev/null +++ b/tests/pyproject/python_toolchain_test/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "python-toolchain-test" +dynamic = ["version"] +requires-python = "==3.11.7" diff --git a/tests/pyproject/python_toolchain_test/test_version.py b/tests/pyproject/python_toolchain_test/test_version.py new file mode 100644 index 0000000000..09ef166f6c --- /dev/null +++ b/tests/pyproject/python_toolchain_test/test_version.py @@ -0,0 +1,19 @@ +"""Test that Python version matches pyproject.toml requires-python.""" + +import sys +import unittest + + +class PythonVersionTest(unittest.TestCase): + def test_python_version_from_pyproject(self): + """Verify we're running Python 3.11.7 as specified in pyproject.toml.""" + version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + self.assertEqual( + version, + "3.11.7", + f"Expected Python 3.11.7 from pyproject.toml, got {version}", + ) + + +if __name__ == "__main__": + unittest.main() From dd44cf9f0c05707e2d6022ac6f57f3ed469fed59 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 7 Feb 2026 22:39:55 -0800 Subject: [PATCH 2/9] add toml2json --- tests/tools/private/toml2json/BUILD.bazel | 10 +++ .../tools/private/toml2json/toml2json_test.py | 62 +++++++++++++++++++ tools/private/toml2json/BUILD.bazel | 9 +++ tools/private/toml2json/toml2json.py | 42 +++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/tools/private/toml2json/BUILD.bazel create mode 100644 tests/tools/private/toml2json/toml2json_test.py create mode 100644 tools/private/toml2json/BUILD.bazel create mode 100644 tools/private/toml2json/toml2json.py diff --git a/tests/tools/private/toml2json/BUILD.bazel b/tests/tools/private/toml2json/BUILD.bazel new file mode 100644 index 0000000000..8c4e94df7d --- /dev/null +++ b/tests/tools/private/toml2json/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "toml2json_test", + srcs = ["toml2json_test.py"], + main = "toml2json_test.py", + deps = [ + "//tools/private/toml2json", + ], +) \ No newline at end of file diff --git a/tests/tools/private/toml2json/toml2json_test.py b/tests/tools/private/toml2json/toml2json_test.py new file mode 100644 index 0000000000..6c63f7e499 --- /dev/null +++ b/tests/tools/private/toml2json/toml2json_test.py @@ -0,0 +1,62 @@ +import io +import json +import os +import sys +import tempfile +import unittest +from unittest.mock import patch + +from tools.private.toml2json import toml2json + +class Toml2JsonTest(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + + def _create_temp_toml_file(self, content): + fd, path = tempfile.mkstemp(suffix=".toml", dir=self.temp_dir.name) + with os.fdopen(fd, "wb") as f: + f.write(content) + return path + + def test_basic_conversion(self): + toml_content = b""" +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 +""" + expected_json = { + "owner": { + "name": "Tom Preston-Werner", + "dob": "1979-05-27T07:32:00-08:00" + } + } + + toml_file_path = self._create_temp_toml_file(toml_content) + + with patch('sys.stdout', new=io.StringIO()) as mock_stdout: + with patch('sys.argv', ['toml2json.py', toml_file_path]): + toml2json.main() + actual_json = json.loads(mock_stdout.getvalue()) + self.assertEqual(actual_json, expected_json) + + def test_invalid_toml(self): + toml_content = b""" +[owner +name = "Tom Preston-Werner" +""" + + toml_file_path = self._create_temp_toml_file(toml_content) + + with patch('sys.stderr', new=io.StringIO()) as mock_stderr: + with patch('sys.stdout', new=io.StringIO()): # We don't expect stdout for errors + with patch('sys.exit') as mock_exit: + with patch('sys.argv', ['toml2json.py', toml_file_path]): + toml2json.main() + mock_exit.assert_called_with(1) + self.assertIn("Error decoding TOML", mock_stderr.getvalue()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/private/toml2json/BUILD.bazel b/tools/private/toml2json/BUILD.bazel new file mode 100644 index 0000000000..428f512923 --- /dev/null +++ b/tools/private/toml2json/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_python//python:defs.bzl", "py_binary") + +exports_files(["toml2json.py"]) + +py_binary( + name = "toml2json", + srcs = ["toml2json.py"], + visibility = ["//visibility:public"], +) diff --git a/tools/private/toml2json/toml2json.py b/tools/private/toml2json/toml2json.py new file mode 100644 index 0000000000..84b7f9c30f --- /dev/null +++ b/tools/private/toml2json/toml2json.py @@ -0,0 +1,42 @@ +import json +import sys +import datetime + +try: + import tomllib +except ImportError: + try: + import tomli as tomllib + except ImportError: + print("Error: need tomllib (python >=3.11) or tomli installed on host python", file=sys.stderr) + sys.exit(1) + + +def json_serializer(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + +def main(): + if len(sys.argv) < 2: + print("Usage: toml2json ", file=sys.stderr) + sys.exit(1) + + toml_file_path = sys.argv[1] + + try: + with open(toml_file_path, "rb") as f: + data = tomllib.load(f) + json.dump(data, sys.stdout, indent=2, default=json_serializer) + print() + except FileNotFoundError: + print(f"Error: File not found: {toml_file_path}", file=sys.stderr) + sys.exit(1) + except tomllib.TOMLDecodeError as e: + print(f"Error decoding TOML: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() From 10bf6202cf1a1cc1836757595d480507bfe6ba22 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 8 Feb 2026 09:01:11 -0800 Subject: [PATCH 3/9] replace version marker, remove allow_single_file=True --- python/private/pypi/extension.bzl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 279bf4c220..8c475fe72f 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -493,7 +493,6 @@ If you are defining custom platforms in your project and don't want things to cl ), "pyproject_toml": attr.label( mandatory = False, - allow_single_file = True, doc = """\ Label pointing to pyproject.toml file to read the default Python version from. When specified, reads the `requires-python` field from pyproject.toml and uses @@ -503,7 +502,7 @@ explicitly specify one. The version must be specified as `==X.Y.Z` (exact version with full semver). This is designed to work with dependency management tools like Renovate. -:::{versionadded} 1.8.0 +:::{versionadded} VERSION_NEXT_FEATURE ::: """, ), @@ -693,6 +692,10 @@ The Python version the dependencies are targetting, in Major.Minor format If an interpreter isn't explicitly provided (using `python_interpreter` or `python_interpreter_target`), then the version specified here must have a corresponding `python.toolchain()` configured. + +:::{seealso} +The {obj}`pyproject_toml` attribute for getting the version from a project file. +::: """, ), "simpleapi_skip": attr.string_list( From e77fd60ce6b173df03444732d95d50c38b987bf5 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 8 Feb 2026 16:16:30 -0800 Subject: [PATCH 4/9] add run_toml2json and attrs to pip.parse extension --- python/private/pypi/extension.bzl | 6 ++++ python/private/pypi/pypi_repo_utils.bzl | 40 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 8c475fe72f..39188169ea 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -744,6 +744,12 @@ The list of labels to use as SRCS for the marker evaluation code. This ensures t code will be re-evaluated when any of files in the default changes. """, ), + "_toml2json": attr.label( + default = "//tools/private/toml2json:toml2json.py", + ), + "_tool_python_interpreter": attr.label( + default = "@python_3_14_host//:BUILD.bazel", + ), }, **ATTRS) attrs.update(AUTH_ATTRS) diff --git a/python/private/pypi/pypi_repo_utils.bzl b/python/private/pypi/pypi_repo_utils.bzl index d8e320014f..c052dd3321 100644 --- a/python/private/pypi/pypi_repo_utils.bzl +++ b/python/private/pypi/pypi_repo_utils.bzl @@ -162,6 +162,45 @@ def _execute_checked_stdout(mrctx, *, python, srcs, **kwargs): **_execute_prep(mrctx, python = python, srcs = srcs, **kwargs) ) +def _run_toml2json(mrctx, toml_label, attr, logger = None): + """Parses a TOML file into a dictionary using toml2json tool. + + Args: + mrctx: The module_ctx or repository_ctx object. + toml_label: Label pointing to the TOML file. + attr: Attributes struct (e.g. from a repository rule). Must contain + `_tool_python_interpreter` and `_toml2json`. + logger: Optional logger instance for informational messages. + + Returns: + A dictionary representation of the TOML file content. + """ + if not toml_label: + fail("toml_label cannot be None") + + python_interpreter = _resolve_python_interpreter( + mrctx, + python_interpreter_target = attr._tool_python_interpreter, + ) + + toml_path = mrctx.path(toml_label) + if not toml_path.exists: + fail("toml file does not exist: {} (from label {})".format(toml_path, toml_label)) + + # Use the shared toml2json tool + toml2json_tool = mrctx.path(ctx.attr._toml2json) + + stdout = _execute_checked_stdout( + mrctx, + logger = logger, + op = "Toml2Json: {}".format(toml_label), + python = python_interpreter, + arguments = [str(toml2json_tool), str(toml_path)], + srcs = [toml2json_tool, toml_path], + ) + + return json.decode(stdout) + def _find_namespace_package_files(rctx, install_dir): """Finds all `__init__.py` files that belong to namespace packages. @@ -202,4 +241,5 @@ pypi_repo_utils = struct( execute_checked_stdout = _execute_checked_stdout, find_namespace_package_files = _find_namespace_package_files, resolve_python_interpreter = _resolve_python_interpreter, + run_toml2json = _run_toml2json, ) From 9788ba49ab94722ac0444ce2512fe1f4af7bde8d Mon Sep 17 00:00:00 2001 From: Jan Winkler Date: Thu, 12 Feb 2026 09:48:02 +0100 Subject: [PATCH 5/9] Fix precedence logic --- 2 | 1460 +++++++++++++++++++++++++++++++++++++ python/private/python.bzl | 14 +- 2 files changed, 1467 insertions(+), 7 deletions(-) create mode 100644 2 diff --git a/2 b/2 new file mode 100644 index 0000000000..3266c831b8 --- /dev/null +++ b/2 @@ -0,0 +1,1460 @@ +# Copyright 2023 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. + +"Python toolchain module extensions for use with bzlmod." + +load("@bazel_features//:features.bzl", "bazel_features") +load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") +load(":auth.bzl", "AUTH_ATTRS") +load(":full_version.bzl", "full_version") +load(":platform_info.bzl", "platform_info") +load(":pyproject_repo.bzl", "pyproject_version_repo") +load(":pyproject_utils.bzl", "read_pyproject_version") +load(":python_register_toolchains.bzl", "python_register_toolchains") +load(":pythons_hub.bzl", "hub_repo") +load(":repo_utils.bzl", "repo_utils") +load( + ":toolchains_repo.bzl", + "host_compatible_python_repo", + "multi_toolchain_aliases", + "sorted_host_platform_names", + "sorted_host_platforms", +) +load(":version.bzl", "version") + +def parse_modules(*, module_ctx, logger, _fail = fail): + """Parse the modules and return a struct for registrations. + + Args: + module_ctx: {type}`module_ctx` module context. + logger: {type}`repo_utils.logger` A logger to use. + _fail: {type}`function` the failure function, mainly for testing. + + Returns: + A struct with the following attributes: + * `toolchains`: {type}`list[ToolchainConfig]` The list of toolchains to + register. The last element is special and is treated as the default + toolchain. + * `config`: Various toolchain config, see `_get_toolchain_config`. + * `debug_info`: {type}`None | dict` extra information to be passed + to the debug repo. + * `platforms`: {type}`dict[str, platform_info]` of the base set of + platforms toolchains should be created for, if possible. + + ToolchainConfig struct: + * python_version: str, full python version string + * name: str, the base toolchain name, e.g., "python_3_10", no + platform suffix. + * register_coverage_tool: bool + """ + if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": + debug_info = { + "toolchains_registered": [], + } + else: + debug_info = None + + # 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 or Major.Minor.Patch to the toolchain_info struct + global_toolchain_versions = {} + + config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) + + default_python_version = _compute_default_python_version(module_ctx) + + seen_versions = {} + for mod in module_ctx.modules: + module_toolchain_versions = [] + toolchain_attr_structs = _create_toolchain_attr_structs( + mod = mod, + seen_versions = seen_versions, + config = config, + module_ctx = module_ctx, + logger = logger, + ) + + for toolchain_attr in toolchain_attr_structs: + toolchain_version = toolchain_attr.python_version + toolchain_name = "python_" + toolchain_version.replace(".", "_") + + # Duplicate versions within a module indicate a misconfigured module. + if toolchain_version in module_toolchain_versions: + _fail_duplicate_module_toolchain_version(toolchain_version, mod.name) + module_toolchain_versions.append(toolchain_version) + + if mod.is_root: + # Only the root module and rules_python are allowed to specify the default + # toolchain for a couple reasons: + # * It prevents submodules from specifying different defaults and only + # one of them winning. + # * rules_python needs to set a soft default in case the root module doesn't, + # e.g. if the root module doesn't use Python itself. + # * The root module is allowed to override the rules_python default. + is_default = default_python_version == toolchain_version + + elif mod.name == "rules_python" and not default_toolchain: + # This branch handles when the root module doesn't declare a + # Python toolchain + is_default = default_python_version == toolchain_version + else: + is_default = False + + if is_default and default_toolchain != None: + _fail_multiple_default_toolchains_chosen( + first = default_toolchain.name, + second = toolchain_name, + ) + + # Ignore version collisions in the global scope because there isn't + # much else that can be done. Modules don't know and can't control + # what other modules do, so the first in the dependency graph wins. + if toolchain_version in global_toolchain_versions: + # If the python version is explicitly provided by the root + # module, they should not be warned for choosing the same + # version that rules_python provides as default. + first = global_toolchain_versions[toolchain_version] + if mod.name != "rules_python" or not first.module.is_root: + # The warning can be enabled by setting the verbosity: + # env RULES_PYTHON_REPO_DEBUG_VERBOSITY=INFO bazel build //... + _warn_duplicate_global_toolchain_version( + toolchain_version, + first = first, + second_toolchain_name = toolchain_name, + second_module_name = mod.name, + logger = logger, + ) + toolchain_info = None + else: + toolchain_info = struct( + python_version = toolchain_attr.python_version, + name = toolchain_name, + register_coverage_tool = toolchain_attr.configure_coverage_tool, + module = struct(name = mod.name, is_root = mod.is_root), + ) + global_toolchain_versions[toolchain_version] = toolchain_info + if debug_info: + debug_info["toolchains_registered"].append({ + "module": {"is_root": mod.is_root, "name": mod.name}, + "name": toolchain_name, + }) + + if is_default: + # This toolchain is setting the default, but the actual + # registration was performed previously, by a different module. + if toolchain_info == None: + default_toolchain = global_toolchain_versions[toolchain_version] + + # Remove it because later code will add it at the end to + # ensure it is last in the list. + toolchains.remove(default_toolchain) + else: + default_toolchain = toolchain_info + elif toolchain_info: + toolchains.append(toolchain_info) + + # A default toolchain is required so that the non-version-specific rules + # are able to match a toolchain. + if default_toolchain == None: + fail("No default Python toolchain configured. Is rules_python missing `python.defaults()`?") + elif default_toolchain.python_version not in global_toolchain_versions: + fail('Default version "{python_version}" selected by module ' + + '"{module_name}", but no toolchain with that version registered'.format( + python_version = default_toolchain.python_version, + module_name = default_toolchain.module.name, + )) + + # The last toolchain in the BUILD file is set as the default + # toolchain. We need the default last. + toolchains.append(default_toolchain) + + # sort the toolchains so that the toolchain versions that are in the + # `minor_mapping` are coming first. This ensures that `python_version = + # "3.X"` transitions work as expected. + minor_version_toolchains = [] + other_toolchains = [] + minor_mapping = list(config.minor_mapping.values()) + for t in toolchains: + # FIXME @aignas 2025-04-04: How can we unit test that this ordering is + # consistent with what would actually work? + if config.minor_mapping.get(t.python_version, t.python_version) in minor_mapping: + minor_version_toolchains.append(t) + else: + other_toolchains.append(t) + toolchains = minor_version_toolchains + other_toolchains + + return struct( + config = config, + debug_info = debug_info, + default_python_version = default_toolchain.python_version, + toolchains = [ + struct( + python_version = t.python_version, + name = t.name, + register_coverage_tool = t.register_coverage_tool, + ) + for t in toolchains + ], + ) + +def _python_impl(module_ctx): + logger = repo_utils.logger(module_ctx, "python") + py = parse_modules(module_ctx = module_ctx, logger = logger) + + # Create pyproject version repo if pyproject.toml is used + created_pyproject_repo = False + for mod in module_ctx.modules: + if mod.is_root: + for tag in mod.tags.defaults: + if tag.pyproject_toml: + pyproject_version_repo( + name = "python_version_from_pyproject", + pyproject_toml = tag.pyproject_toml, + ) + created_pyproject_repo = True + break + break + + # Host compatible runtime repos + # dict[str version, struct] where struct has: + # * full_python_version: str + # * platform: platform_info struct + # * platform_name: str platform name + # * impl_repo_name: str repo name of the runtime's python_repository() repo + all_host_compatible_impls = {} + + # Host compatible repos that still need to be created because, when + # creating the actual runtime repo, there wasn't a host-compatible + # variant defined for it. + # dict[str reponame, struct] where struct has: + # * compatible_version: str, e.g. 3.10 or 3.10.1. The version the host + # repo should be compatible with + # * full_python_version: str, e.g. 3.10.1, the full python version of + # the toolchain that still needs a host repo created. + needed_host_repos = {} + + # list of structs; see inline struct call within the loop below. + toolchain_impls = [] + + # list[str] of the repo names for host compatible repos + all_host_compatible_repo_names = [] + + # Create the underlying python_repository repos that contain the + # python runtimes and their toolchain implementation definitions. + for i, toolchain_info in enumerate(py.toolchains): + is_last = (i + 1) == len(py.toolchains) + + # Ensure that we pass the full version here. + full_python_version = full_version( + version = toolchain_info.python_version, + minor_mapping = py.config.minor_mapping, + fail_on_err = False, + ) + if not full_python_version: + logger.info(lambda: ( + "The actual toolchain for python_version '{version}' " + + "has not been registered, but was requested, please configure a toolchain " + + "to be actually downloaded and setup" + ).format( + version = toolchain_info.python_version, + )) + continue + + kwargs = { + "python_version": full_python_version, + "register_coverage_tool": toolchain_info.register_coverage_tool, + } + + # Allow overrides per python version + kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) + kwargs.update(py.config.kwargs.get(full_python_version, {})) + kwargs.update(py.config.default) + register_result = python_register_toolchains( + name = toolchain_info.name, + _internal_bzlmod_toolchain_call = True, + **kwargs + ) + if not register_result.impl_repos: + continue + + host_platforms = {} + for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): + toolchain_impls.append(struct( + # str: The base name to use for the toolchain() target + name = repo_name, + # str: The repo name the toolchain() target points to. + impl_repo_name = repo_name, + # str: platform key in the passed-in platforms dict + platform_name = platform_name, + # struct: platform_info() struct + platform = platform_info, + # str: Major.Minor.Micro python version + full_python_version = full_python_version, + # bool: whether to implicitly add the python version constraint + # to the toolchain's target_settings. + # The last toolchain is the default; it can't have version constraints + set_python_version_constraint = is_last, + )) + if _is_compatible_with_host(module_ctx, platform_info): + host_compat_entry = struct( + full_python_version = full_python_version, + platform = platform_info, + platform_name = platform_name, + impl_repo_name = repo_name, + ) + host_platforms[platform_name] = host_compat_entry + all_host_compatible_impls.setdefault(full_python_version, []).append( + host_compat_entry, + ) + parsed_version = version.parse(full_python_version) + all_host_compatible_impls.setdefault( + "{}.{}".format(*parsed_version.release[0:2]), + [], + ).append(host_compat_entry) + + host_repo_name = toolchain_info.name + "_host" + if host_platforms: + all_host_compatible_repo_names.append(host_repo_name) + host_platforms = sorted_host_platforms(host_platforms) + entries = host_platforms.values() + host_compatible_python_repo( + name = host_repo_name, + base_name = host_repo_name, + # NOTE: Order matters. The first found to be compatible is + # (usually) used. + platforms = host_platforms.keys(), + os_names = {str(i): e.platform.os_name for i, e in enumerate(entries)}, + arch_names = {str(i): e.platform.arch for i, e in enumerate(entries)}, + python_versions = {str(i): e.full_python_version for i, e in enumerate(entries)}, + impl_repo_names = {str(i): e.impl_repo_name for i, e in enumerate(entries)}, + ) + else: + needed_host_repos[host_repo_name] = struct( + compatible_version = toolchain_info.python_version, + full_python_version = full_python_version, + ) + + if needed_host_repos: + for key, entries in all_host_compatible_impls.items(): + all_host_compatible_impls[key] = sorted( + entries, + reverse = True, + key = lambda e: version.key(version.parse(e.full_python_version)), + ) + + for host_repo_name, info in needed_host_repos.items(): + choices = [] + if info.compatible_version not in all_host_compatible_impls: + logger.warn("No host compatible runtime found compatible with version {}".format(info.compatible_version)) + continue + + choices = all_host_compatible_impls[info.compatible_version] + platform_keys = [ + # We have to prepend the offset because the same platform + # name might occur across different versions + "{}_{}".format(i, entry.platform_name) + for i, entry in enumerate(choices) + ] + platform_keys = sorted_host_platform_names(platform_keys) + + all_host_compatible_repo_names.append(host_repo_name) + host_compatible_python_repo( + name = host_repo_name, + base_name = host_repo_name, + platforms = platform_keys, + impl_repo_names = { + str(i): entry.impl_repo_name + for i, entry in enumerate(choices) + }, + os_names = {str(i): entry.platform.os_name for i, entry in enumerate(choices)}, + arch_names = {str(i): entry.platform.arch for i, entry in enumerate(choices)}, + python_versions = {str(i): entry.full_python_version for i, entry in enumerate(choices)}, + ) + + # list[str] The infix to use for the resulting toolchain() `name` arg. + toolchain_names = [] + + # dict[str i, str repo]; where repo is the full repo name + # ("python_3_10_unknown-linux-x86_64") for the toolchain + # i corresponds to index `i` in toolchain_names + toolchain_repo_names = {} + + # dict[str i, list[str] constraints]; where constraints is a list + # of labels for target_compatible_with + # i corresponds to index `i` in toolchain_names + toolchain_tcw_map = {} + + # dict[str i, list[str] settings]; where settings is a list + # of labels for target_settings + # i corresponds to index `i` in toolchain_names + toolchain_ts_map = {} + + # dict[str i, str set_constraint]; where set_constraint is the string + # "True" or "False". + # i corresponds to index `i` in toolchain_names + toolchain_set_python_version_constraints = {} + + # dict[str i, str python_version]; where python_version is the full + # python version ("3.4.5"). + toolchain_python_versions = {} + + # dict[str i, str platform_key]; where platform_key is the key within + # the PLATFORMS global for this toolchain + toolchain_platform_keys = {} + + # Split the toolchain info into separate objects so they can be passed onto + # the repository rule. + for entry in toolchain_impls: + key = str(len(toolchain_names)) + + toolchain_names.append(entry.name) + toolchain_repo_names[key] = entry.impl_repo_name + toolchain_tcw_map[key] = entry.platform.compatible_with + + # The target_settings attribute may not be present for users + # patching python/versions.bzl. + toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + toolchain_platform_keys[key] = entry.platform_name + toolchain_python_versions[key] = entry.full_python_version + + # Repo rules can't accept dict[str, bool], so encode them as a string value. + toolchain_set_python_version_constraints[key] = ( + "True" if entry.set_python_version_constraint else "False" + ) + + hub_repo( + name = "pythons_hub", + toolchain_names = toolchain_names, + toolchain_repo_names = toolchain_repo_names, + toolchain_target_compatible_with_map = toolchain_tcw_map, + toolchain_target_settings_map = toolchain_ts_map, + toolchain_platform_keys = toolchain_platform_keys, + toolchain_python_versions = toolchain_python_versions, + toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, + host_compatible_repo_names = sorted(all_host_compatible_repo_names), + default_python_version = py.default_python_version, + minor_mapping = py.config.minor_mapping, + python_versions = list(py.config.default["tool_versions"].keys()), + ) + + # This is require in order to support multiple version py_test + # and py_binary + multi_toolchain_aliases( + name = "python_versions", + python_versions = { + toolchain.python_version: toolchain.name + for toolchain in py.toolchains + }, + ) + + if py.debug_info != None: + _debug_repo( + name = "rules_python_bzlmod_debug", + debug_info = json.encode_indent(py.debug_info), + ) + + if bazel_features.external_deps.extension_metadata_has_reproducible: + # Build the list of direct dependencies + root_direct_deps = ["pythons_hub", "python_versions"] + if created_pyproject_repo: + root_direct_deps.append("python_version_from_pyproject") + + return module_ctx.extension_metadata( + root_module_direct_deps = root_direct_deps, + root_module_direct_dev_deps = [], + reproducible = True, + ) + else: + return None + +def _is_compatible_with_host(mctx, platform_info): + os_name = repo_utils.get_platforms_os_name(mctx) + cpu_name = repo_utils.get_platforms_cpu_name(mctx) + return platform_info.os_name == os_name and platform_info.arch == cpu_name + +def _one_or_the_same(first, second, *, onerror = None): + if not first: + return second + if not second or second == first: + return first + if onerror: + return onerror(first, second) + else: + fail("Unique value needed, got both '{}' and '{}', which are different".format( + first, + second, + )) + +def _fail_duplicate_module_toolchain_version(version, module): + fail(("Duplicate module toolchain version: module '{module}' attempted " + + "to use version '{version}' multiple times in itself").format( + version = version, + module = 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}' " + + "already registered Python version {version} and has precedence." + ).format( + first_toolchain = first.name, + first_module = first.module.name, + second_module = second_module_name, + second_toolchain = second_toolchain_name, + version = version, + )) + +def _fail_multiple_defaults_python_version(first, second): + fail(("Multiple python_version entries in defaults: " + + "First default was python_version '{first}'. " + + "Second was python_version '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_defaults_python_version_file(first, second): + fail(("Multiple python_version_file entries in defaults: " + + "First default was python_version_file '{first}'. " + + "Second was python_version_file '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_defaults_python_version_env(first, second): + fail(("Multiple python_version_env entries in defaults: " + + "First default was python_version_env '{first}'. " + + "Second was python_version_env '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_default_toolchains_chosen(first, second): + fail(("Multiple default toolchains: only one toolchain " + + "can be chosen as a default. First default " + + "was toolchain '{first}'. Second was '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs): + fail(("Multiple default toolchains: only one toolchain " + + "can have is_default=True.\n" + + "Module '{module}' contains {count} toolchains with " + + "is_default=True: {versions}").format( + module = mod.name, + count = len(toolchain_attrs), + versions = ", ".join(sorted([v.python_version for v in toolchain_attrs])), + )) + +def _validate_version(version_str, *, _fail = fail): + v = version.parse(version_str, strict = True, _fail = _fail) + if v == None: + # Only reachable in tests + return False + + if len(v.release) < 3: + _fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string)) + return False + + return True + +def _process_single_version_overrides(*, tag, _fail = fail, default): + if not _validate_version(tag.python_version, _fail = _fail): + return + + available_versions = default["tool_versions"] + kwargs = default.setdefault("kwargs", {}) + + if tag.sha256 or tag.urls: + if not (tag.sha256 and tag.urls): + _fail("Both `sha256` and `urls` overrides need to be provided together") + return + + for platform in (tag.sha256 or []): + if platform not in default["platforms"]: + _fail("The platform must be one of {allowed} but got '{got}'".format( + allowed = sorted(default["platforms"]), + got = platform, + )) + return + + 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 + +def _process_single_version_platform_overrides(*, tag, _fail = fail, default): + if not _validate_version(tag.python_version, _fail = _fail): + return + + available_versions = default["tool_versions"] + + 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)) + return + 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 + + # If platform is customized, or doesn't exist, (re)define one. + if ((tag.target_compatible_with or tag.target_settings or tag.os_name or tag.arch) or + tag.platform not in default["platforms"]): + os_name = tag.os_name + arch = tag.arch + + if not tag.target_compatible_with: + target_compatible_with = [] + if os_name: + target_compatible_with.append("@platforms//os:{}".format( + repo_utils.get_platforms_os_name(os_name), + )) + if arch: + target_compatible_with.append("@platforms//cpu:{}".format( + repo_utils.get_platforms_cpu_name(arch), + )) + else: + target_compatible_with = tag.target_compatible_with + + # For lack of a better option, give a bogus value. It only affects + # if the runtime is considered host-compatible. + if not os_name: + os_name = "UNKNOWN_CUSTOM_OS" + if not arch: + arch = "UNKNOWN_CUSTOM_ARCH" + + # Move the override earlier in the ordering -- the platform key ordering + # becomes the toolchain ordering within the version. This allows the + # override to have a superset of constraints from a regular runtimes + # (e.g. same platform, but with a custom flag required). + override_first = { + tag.platform: platform_info( + compatible_with = target_compatible_with, + target_settings = tag.target_settings, + os_name = os_name, + arch = arch, + ), + } + for key, value in default["platforms"].items(): + # Don't replace our override with the old value + if key in override_first: + continue + override_first[key] = value + + default["platforms"] = override_first + +def _process_global_overrides(*, tag, default, _fail = fail): + if tag.available_python_versions: + available_versions = default["tool_versions"] + all_versions = dict(available_versions) + available_versions.clear() + for v in tag.available_python_versions: + if v not in all_versions: + _fail("unknown version '{}', known versions are: {}".format( + v, + sorted(all_versions), + )) + return + + available_versions[v] = all_versions[v] + + if tag.minor_mapping: + for minor_version, full_version in tag.minor_mapping.items(): + parsed = version.parse(minor_version, strict = True, _fail = _fail) + if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local: + fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string)) + + # Ensure that the version is valid + version.parse(full_version, strict = True, _fail = _fail) + + default["minor_mapping"] = tag.minor_mapping + + forwarded_attrs = sorted(AUTH_ATTRS) + [ + "base_url", + "register_all_versions", + ] + for key in forwarded_attrs: + if getattr(tag, key, None): + default[key] = getattr(tag, key) + +def _override_defaults(*overrides, modules, _fail = fail, default): + mod = modules[0] if modules else None + if not mod or not mod.is_root: + return + + overriden_keys = [] + + for override in overrides: + for tag in getattr(mod.tags, override.name): + key = override.key(tag) + if key not in overriden_keys: + overriden_keys.append(key) + elif key: + _fail("Only a single 'python.{}' can be present for '{}'".format(override.name, key)) + return + else: + _fail("Only a single 'python.{}' can be present".format(override.name)) + return + + override.fn(tag = tag, _fail = _fail, default = default) + +def _get_toolchain_config(*, modules, _fail = fail): + """Computes the configs for toolchains. + + Args: + modules: The modules from module_ctx + _fail: Function to call for failing; only used for testing. + + Returns: + A struct with the following: + * `kwargs`: {type}`dict[str, dict[str, object]` custom kwargs to pass to + `python_register_toolchains`, keyed by python version. + The first key is either a Major.Minor or Major.Minor.Patch + string. + * `minor_mapping`: {type}`dict[str, str]` the mapping of Major.Minor + to Major.Minor.Patch. + * `default`: {type}`dict[str, object]` of kwargs passed along to + `python_register_toolchains`. These keys take final precedence. + * `register_all_versions`: {type}`bool` whether all known versions + should be registered. + """ + + # Items that can be overridden + available_versions = {} + for py_version, item in TOOL_VERSIONS.items(): + available_versions[py_version] = {} + available_versions[py_version]["sha256"] = dict(item["sha256"]) + platforms = item["sha256"].keys() + + strip_prefix = item["strip_prefix"] + if type(strip_prefix) == type(""): + available_versions[py_version]["strip_prefix"] = { + platform: strip_prefix + for platform in platforms + } + else: + available_versions[py_version]["strip_prefix"] = dict(strip_prefix) + url = item["url"] + if type(url) == type(""): + available_versions[py_version]["url"] = { + platform: url + for platform in platforms + } + else: + available_versions[py_version]["url"] = dict(url) + + default = { + "base_url": DEFAULT_RELEASE_BASE_URL, + "platforms": dict(PLATFORMS), # Copy so it's mutable. + "tool_versions": available_versions, + } + + _override_defaults( + # First override by single version, because the sha256 will replace + # anything that has been there before. + struct( + name = "single_version_override", + key = lambda t: t.python_version, + fn = _process_single_version_overrides, + ), + # Then override particular platform entries if they need to be overridden. + struct( + name = "single_version_platform_override", + key = lambda t: (t.python_version, t.platform), + fn = _process_single_version_platform_overrides, + ), + # Then finally add global args and remove the unnecessary toolchains. + # This ensures that we can do further validations when removing. + struct( + name = "override", + key = lambda t: None, + fn = _process_global_overrides, + ), + modules = modules, + default = default, + _fail = _fail, + ) + + register_all_versions = default.pop("register_all_versions", False) + kwargs = default.pop("kwargs", {}) + + versions = {} + for version_string in available_versions: + v = version.parse(version_string, strict = True) + versions.setdefault( + "{}.{}".format(v.release[0], v.release[1]), + [], + ).append((version.key(v), v.string)) + + minor_mapping = { + major_minor: max(subset)[1] + for major_minor, subset in versions.items() + } + + # The following ensures that all of the versions will be present in the minor_mapping + minor_mapping_overrides = default.pop("minor_mapping", {}) + for major_minor, full in minor_mapping_overrides.items(): + minor_mapping[major_minor] = full + + return struct( + kwargs = kwargs, + minor_mapping = minor_mapping, + default = default, + register_all_versions = register_all_versions, + ) + +def _compute_default_python_version(mctx): + default_python_version = None + for mod in mctx.modules: + # Only the root module and rules_python are allowed to specify the default + # toolchain for a couple reasons: + # * It prevents submodules from specifying different defaults and only + # one of them winning. + # * rules_python needs to set a soft default in case the root module doesn't, + # e.g. if the root module doesn't use Python itself. + # * The root module is allowed to override the rules_python default. + if not (mod.is_root or mod.name == "rules_python"): + continue + + defaults_attr_structs = _create_defaults_attr_structs(mod = mod) + default_python_version_env = None + default_python_version_file = None + pyproject_toml_label = None + + for defaults_attr in defaults_attr_structs: + pyproject_toml_label = _one_or_the_same( + pyproject_toml_label, + defaults_attr.pyproject_toml, + onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), + ) + + default_python_version = _one_or_the_same( + default_python_version, + defaults_attr.python_version, + onerror = _fail_multiple_defaults_python_version, + ) + default_python_version_env = _one_or_the_same( + default_python_version_env, + defaults_attr.python_version_env, + onerror = _fail_multiple_defaults_python_version_env, + ) + default_python_version_file = _one_or_the_same( + default_python_version_file, + defaults_attr.python_version_file, + onerror = _fail_multiple_defaults_python_version_file, + ) + + if default_python_version_file: + default_python_version = _one_or_the_same( + default_python_version, + mctx.read(default_python_version_file, watch = "yes").strip(), + ) + if pyproject_toml_label: + pyproject_version = read_pyproject_version( + mctx, + pyproject_toml_label, + logger = None, + ) + if pyproject_version: + default_python_version = pyproject_version + if default_python_version_env: + default_python_version = mctx.getenv( + default_python_version_env, + default_python_version, + ) + + if default_python_version: + break + + # Otherwise, look at legacy python.toolchain() calls for a default + toolchain_attrs = mod.tags.toolchain + + # Convenience: if one python.toolchain() call exists, treat it as + # the default. + if len(toolchain_attrs) == 1: + default_python_version = toolchain_attrs[0].python_version + else: + sets_default = [v for v in toolchain_attrs if v.is_default] + if len(sets_default) == 1: + default_python_version = sets_default[0].python_version + elif len(sets_default) > 1: + _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs) + + if default_python_version: + break + + return default_python_version + +def _create_defaults_attr_structs(*, mod): + arg_structs = [] + + for tag in mod.tags.defaults: + arg_structs.append(_create_defaults_attr_struct(tag = tag)) + + return arg_structs + +def _create_defaults_attr_struct(*, tag): + return struct( + python_version = getattr(tag, "python_version", None), + python_version_env = getattr(tag, "python_version_env", None), + python_version_file = getattr(tag, "python_version_file", None), + pyproject_toml = getattr(tag, "pyproject_toml", None), + ) + +def _create_toolchain_attr_structs(*, mod, config, seen_versions, module_ctx, logger): + arg_structs = [] + + # Check if pyproject_toml was specified in defaults + # If so, register a toolchain for it + for tag in mod.tags.defaults: + pyproject_toml = getattr(tag, "pyproject_toml", None) + if pyproject_toml: + pyproject_version = read_pyproject_version( + module_ctx, + pyproject_toml, + logger, + ) + if pyproject_version and pyproject_version not in seen_versions: + arg_structs.append(_create_toolchain_attrs_struct( + python_version = pyproject_version, + toolchain_tag_count = 1, + )) + seen_versions[pyproject_version] = True + + 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 + + if config.register_all_versions: + arg_structs.extend([ + _create_toolchain_attrs_struct(python_version = v) + for v in config.default["tool_versions"].keys() + config.minor_mapping.keys() + if v not in seen_versions + ]) + + return arg_structs + +def _create_toolchain_attrs_struct( + *, + tag = None, + python_version = None, + toolchain_tag_count = None): + if tag and python_version: + fail("Only one of tag and python version can be specified") + if tag: + # A single toolchain is treated as the default because it's unambiguous. + is_default = tag.is_default or toolchain_tag_count == 1 + else: + is_default = False + + return struct( + is_default = is_default, + python_version = python_version if python_version else tag.python_version, + configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), + ) + +_defaults = tag_class( + doc = """Tag class to specify the default Python version.""", + attrs = { + "pyproject_toml": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml. +The version must be specified as `==X.Y.Z` (exact version with full semver). + +:::{versionadded} 1.8.0 +::: +""", + ), + "python_version": attr.string( + mandatory = False, + doc = """\ +String saying what the default Python version should be. If the string +matches the {attr}`python_version` attribute of a toolchain, this +toolchain is the default version. If this attribute is set, the +{attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} 1.4.0 +::: +""", + ), + "python_version_env": attr.string( + mandatory = False, + doc = """\ +Environment variable saying what the default Python version should be. +If the string matches the {attr}`python_version` attribute of a +toolchain, this toolchain is the default version. If this attribute is +set, the {attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} 1.4.0 +::: +""", + ), + "python_version_file": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +File saying what the default Python version should be. If the contents +of the file match the {attr}`python_version` attribute of a toolchain, +this toolchain is the default version. If this attribute is set, the +{attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} 1.4.0 +::: +""", + ), + }, +) + +_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. + +:::{topic} Toolchains in the Root Module + +This class registers all toolchains in the root module. +::: + +:::{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. +::: + +:::{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. +::: + +:::{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.defaults(python_version = "3.11") +python.toolchain(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 provided by `rules_python` for the compatible toolchains.", + ), + "ignore_root_user_error": attr.bool( + default = True, + doc = """\ +:::{versionchanged} 1.8.0 +Noop, will be removed in the next major release. +::: +""", + mandatory = False, + ), + "is_default": attr.bool( + mandatory = False, + doc = """\ +Whether the toolchain is the default version. + +:::{versionchanged} 1.4.0 +This setting is ignored if the default version is set using the `defaults` +tag class (encouraged). +::: +""", + ), + "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 = { + "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 = True, + doc = """Deprecated; do not use. This attribute has no effect.""", + 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", +} +``` + +:::{versionchanged} 0.37.0 +The values in this mapping override the default values and do not replace them. +::: +""", + 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 = { + "arch": attr.string( + doc = """ +The arch (cpu) the runtime is compatible with. + +If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. + +If set, the `os_name`, `target_compatible_with` and `target_settings` attributes +should also be set. + +The values should be one of the values in `@platforms//cpu` + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} 1.5.0 +::: +""", + ), + "coverage_tool": attr.label( + doc = """\ +The coverage tool to be used for a particular Python interpreter. This can override +`rules_python` defaults. +""", + ), + "os_name": attr.string( + doc = """ +The host OS the runtime is compatible with. + +If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. + +If set, the `os_name`, `target_compatible_with` and `target_settings` attributes +should also be set. + +The values should be one of the values in `@platforms//os` + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} 1.5.0 +::: +""", + ), + "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, + doc = """ +The platform to override the values for, typically one of:\n +{platforms} + +Other values are allowed, in which case, `target_compatible_with`, +`target_settings`, `os_name`, and `arch` should be specified so the toolchain is +only used when appropriate. + +:::{{versionchanged}} 1.5.0 +Arbitrary platform strings allowed. +::: +""".format( + platforms = "\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", + ), + "target_compatible_with": attr.string_list( + doc = """ +The `target_compatible_with` values to use for the toolchain definition. + +If not set, then `os_name` and `arch` will be used to populate it. + +If set, `target_settings`, `os_name`, and `arch` should also be set. + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} 1.5.0 +::: +""", + ), + "target_settings": attr.string_list( + doc = """ +The `target_setings` values to use for the toolchain definition. + +If set, `target_compatible_with`, `os_name`, and `arch` should also be set. + +:::{seealso} +Docs for [Registering custom runtimes] +::: + +:::{{versionadded}} 1.5.0 +::: +""", + ), + "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 = { + "defaults": _defaults, + "override": _override, + "single_version_override": _single_version_override, + "single_version_platform_override": _single_version_platform_override, + "toolchain": _toolchain, + }, + environ = ["RULES_PYTHON_BZLMOD_DEBUG"], +) + +_DEBUG_BUILD_CONTENT = """ +package( + default_visibility = ["//visibility:public"], +) +exports_files(["debug_info.json"]) +""" + +def _debug_repo_impl(repo_ctx): + repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT) + repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info) + +_debug_repo = repository_rule( + implementation = _debug_repo_impl, + attrs = { + "debug_info": attr.string(), + }, +) diff --git a/python/private/python.bzl b/python/private/python.bzl index 8b226336e0..f6394f0231 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -904,7 +904,12 @@ def _compute_default_python_version(mctx): onerror = _fail_multiple_defaults_python_version_file, ) - # Priority order: pyproject_toml > python_version_file > python_version_env > python_version + # Priority order: ENV > pyproject_toml > python_version_file > python_version + if default_python_version_file: + default_python_version = _one_or_the_same( + default_python_version, + mctx.read(default_python_version_file, watch = "yes").strip(), + ) if pyproject_toml_label: pyproject_version = read_pyproject_version( mctx, @@ -913,12 +918,7 @@ def _compute_default_python_version(mctx): ) if pyproject_version: default_python_version = pyproject_version - elif default_python_version_file: - default_python_version = _one_or_the_same( - default_python_version, - mctx.read(default_python_version_file, watch = "yes").strip(), - ) - elif default_python_version_env: + if default_python_version_env: default_python_version = mctx.getenv( default_python_version_env, default_python_version, From 646d15468b648ff1575f46b4d4bf9f374727be1e Mon Sep 17 00:00:00 2001 From: Jan Winkler Date: Thu, 12 Feb 2026 10:34:50 +0100 Subject: [PATCH 6/9] Remove integration tests --- python/private/pyproject_version_extractor.py | 45 ------ tests/pyproject/BUILD.bazel | 0 .../compile_requirements_test/BUILD.bazel | 14 -- .../compile_requirements_test/MODULE.bazel | 13 -- .../compile_requirements_test/pyproject.toml | 8 - .../requirements_lock.txt | 137 ------------------ .../test_compile_requirements.py | 27 ---- .../pip_integration_test/BUILD.bazel | 9 -- .../pip_integration_test/MODULE.bazel | 21 --- .../pip_integration_test/pyproject.toml | 8 - .../requirements_lock.txt | 137 ------------------ .../pip_integration_test/test_pip_works.py | 21 --- tests/pyproject/priority_test/.python-version | 1 - tests/pyproject/priority_test/BUILD.bazel | 6 - tests/pyproject/priority_test/MODULE.bazel | 16 -- tests/pyproject/priority_test/pyproject.toml | 4 - .../pyproject/priority_test/test_priority.py | 21 --- .../python_toolchain_test/BUILD.bazel | 7 - .../python_toolchain_test/MODULE.bazel | 13 -- .../python_toolchain_test/pyproject.toml | 4 - .../python_toolchain_test/test_version.py | 19 --- 21 files changed, 531 deletions(-) delete mode 100644 python/private/pyproject_version_extractor.py delete mode 100644 tests/pyproject/BUILD.bazel delete mode 100644 tests/pyproject/compile_requirements_test/BUILD.bazel delete mode 100644 tests/pyproject/compile_requirements_test/MODULE.bazel delete mode 100644 tests/pyproject/compile_requirements_test/pyproject.toml delete mode 100644 tests/pyproject/compile_requirements_test/requirements_lock.txt delete mode 100644 tests/pyproject/compile_requirements_test/test_compile_requirements.py delete mode 100644 tests/pyproject/pip_integration_test/BUILD.bazel delete mode 100644 tests/pyproject/pip_integration_test/MODULE.bazel delete mode 100644 tests/pyproject/pip_integration_test/pyproject.toml delete mode 100644 tests/pyproject/pip_integration_test/requirements_lock.txt delete mode 100644 tests/pyproject/pip_integration_test/test_pip_works.py delete mode 100644 tests/pyproject/priority_test/.python-version delete mode 100644 tests/pyproject/priority_test/BUILD.bazel delete mode 100644 tests/pyproject/priority_test/MODULE.bazel delete mode 100644 tests/pyproject/priority_test/pyproject.toml delete mode 100644 tests/pyproject/priority_test/test_priority.py delete mode 100644 tests/pyproject/python_toolchain_test/BUILD.bazel delete mode 100644 tests/pyproject/python_toolchain_test/MODULE.bazel delete mode 100644 tests/pyproject/python_toolchain_test/pyproject.toml delete mode 100644 tests/pyproject/python_toolchain_test/test_version.py diff --git a/python/private/pyproject_version_extractor.py b/python/private/pyproject_version_extractor.py deleted file mode 100644 index 1cfdbd1fbc..0000000000 --- a/python/private/pyproject_version_extractor.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -"""Extract Python version from pyproject.toml.""" - -import re -import sys - -try: - import tomllib as toml -except ImportError: - try: - import tomli as toml - except ImportError: - raise SystemExit( - "need tomllib (python >=3.11) or tomli installed on host python" - ) - - -def validate_and_extract(pyproject_path): - """Validate format and extract version.""" - with open(pyproject_path, "rb") as f: - data = toml.load(f) - - version = data["project"]["requires-python"] - - if not version.startswith("=="): - sys.exit(f"requires-python must use '==' for exact version, got: {version}") - - bare_version = version[2:].strip() - - if not re.match(r"^\d+\.\d+\.\d+$", bare_version): - sys.exit(f"requires-python must be in X.Y.Z format, got: {bare_version}") - - return bare_version - - -def main(): - if len(sys.argv) < 2: - sys.exit("Usage: pyproject_version_extractor.py ") - - version = validate_and_extract(sys.argv[1]) - print(version) - - -if __name__ == "__main__": - main() diff --git a/tests/pyproject/BUILD.bazel b/tests/pyproject/BUILD.bazel deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/pyproject/compile_requirements_test/BUILD.bazel b/tests/pyproject/compile_requirements_test/BUILD.bazel deleted file mode 100644 index 970bcd287f..0000000000 --- a/tests/pyproject/compile_requirements_test/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -load("@python_version_from_pyproject//:version.bzl", "PYTHON_VERSION") -load("@rules_python//python:defs.bzl", "py_test") -load("@rules_python//python:pip.bzl", "compile_pip_requirements") - -compile_pip_requirements( - name = "requirements", - python_version = PYTHON_VERSION, - requirements_txt = "requirements_lock.txt", -) - -py_test( - name = "test_compile_requirements", - srcs = ["test_compile_requirements.py"], -) diff --git a/tests/pyproject/compile_requirements_test/MODULE.bazel b/tests/pyproject/compile_requirements_test/MODULE.bazel deleted file mode 100644 index bccb07e00e..0000000000 --- a/tests/pyproject/compile_requirements_test/MODULE.bazel +++ /dev/null @@ -1,13 +0,0 @@ -"""Test that pyproject.toml version can be used in compile_pip_requirements.""" - -module(name = "compile_requirements_test") - -bazel_dep(name = "rules_python", version = "") -local_path_override( - module_name = "rules_python", - path = "../../..", -) - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.defaults(pyproject_toml = "//:pyproject.toml") -use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/compile_requirements_test/pyproject.toml b/tests/pyproject/compile_requirements_test/pyproject.toml deleted file mode 100644 index 6640278aae..0000000000 --- a/tests/pyproject/compile_requirements_test/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "compile-requirements-test" -dynamic = ["version"] -requires-python = "==3.11.7" - -dependencies = [ - "requests==2.32.5" -] diff --git a/tests/pyproject/compile_requirements_test/requirements_lock.txt b/tests/pyproject/compile_requirements_test/requirements_lock.txt deleted file mode 100644 index 65cbf853f4..0000000000 --- a/tests/pyproject/compile_requirements_test/requirements_lock.txt +++ /dev/null @@ -1,137 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# bazel run //:requirements.update -# -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 - # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 - # via requests -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via compile-requirements-test (pyproject.toml) -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests diff --git a/tests/pyproject/compile_requirements_test/test_compile_requirements.py b/tests/pyproject/compile_requirements_test/test_compile_requirements.py deleted file mode 100644 index 4a7924502c..0000000000 --- a/tests/pyproject/compile_requirements_test/test_compile_requirements.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Test that compile_pip_requirements works with PYTHON_VERSION from pyproject.toml.""" - -import sys -import unittest - - -class CompileRequirementsTest(unittest.TestCase): - def test_python_version_matches_pyproject(self): - """Verify we're using the Python version from pyproject.toml.""" - version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - self.assertEqual( - version, - "3.11.7", - f"Expected Python 3.11.7 from pyproject.toml, got {version}", - ) - - def test_requirements_target_exists(self): - """ - This test just needs to run successfully. - The fact that it runs means compile_pip_requirements worked, - which means PYTHON_VERSION was successfully loaded from the repo. - """ - self.assertTrue(True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pyproject/pip_integration_test/BUILD.bazel b/tests/pyproject/pip_integration_test/BUILD.bazel deleted file mode 100644 index 215e0fb910..0000000000 --- a/tests/pyproject/pip_integration_test/BUILD.bazel +++ /dev/null @@ -1,9 +0,0 @@ -load("@rules_python//python:defs.bzl", "py_test") - -py_test( - name = "test_pip_works", - srcs = ["test_pip_works.py"], - deps = [ - "@pip//requests", - ], -) diff --git a/tests/pyproject/pip_integration_test/MODULE.bazel b/tests/pyproject/pip_integration_test/MODULE.bazel deleted file mode 100644 index c39ba1d18a..0000000000 --- a/tests/pyproject/pip_integration_test/MODULE.bazel +++ /dev/null @@ -1,21 +0,0 @@ -"""Test that pyproject.toml sets pip python_version.""" - -module(name = "pip_integration_test") - -bazel_dep(name = "rules_python", version = "") -local_path_override( - module_name = "rules_python", - path = "../../..", -) - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.defaults(pyproject_toml = "//:pyproject.toml") -use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") - -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") -pip.default(pyproject_toml = "//:pyproject.toml") -pip.parse( - hub_name = "pip", - requirements_lock = "//:requirements_lock.txt", -) -use_repo(pip, "pip") diff --git a/tests/pyproject/pip_integration_test/pyproject.toml b/tests/pyproject/pip_integration_test/pyproject.toml deleted file mode 100644 index 543898e95f..0000000000 --- a/tests/pyproject/pip_integration_test/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "pip-integration-test" -dynamic = ["version"] -requires-python = "==3.11.7" - -dependencies = [ - "requests==2.32.5" -] diff --git a/tests/pyproject/pip_integration_test/requirements_lock.txt b/tests/pyproject/pip_integration_test/requirements_lock.txt deleted file mode 100644 index fa0204bbea..0000000000 --- a/tests/pyproject/pip_integration_test/requirements_lock.txt +++ /dev/null @@ -1,137 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --generate-hashes --output-file=requirements_lock.txt pyproject.toml -# -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 - # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 - # via requests -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via compile-requirements-test (pyproject.toml) -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests diff --git a/tests/pyproject/pip_integration_test/test_pip_works.py b/tests/pyproject/pip_integration_test/test_pip_works.py deleted file mode 100644 index 63d55c87ca..0000000000 --- a/tests/pyproject/pip_integration_test/test_pip_works.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test that pip dependencies resolve correctly with pyproject.toml.""" - -import sys -import unittest - - -class PipIntegrationTest(unittest.TestCase): - def test_python_version(self): - """Verify Python version from pyproject.toml.""" - version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - self.assertEqual(version, "3.11.7") - - def test_can_import_dependency(self): - """Verify pip dependency was resolved.""" - import requests - - self.assertTrue(callable(requests.get)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pyproject/priority_test/.python-version b/tests/pyproject/priority_test/.python-version deleted file mode 100644 index 655ff07381..0000000000 --- a/tests/pyproject/priority_test/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13.9 diff --git a/tests/pyproject/priority_test/BUILD.bazel b/tests/pyproject/priority_test/BUILD.bazel deleted file mode 100644 index 56685c1686..0000000000 --- a/tests/pyproject/priority_test/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("@rules_python//python:defs.bzl", "py_test") - -py_test( - name = "test_priority", - srcs = ["test_priority.py"], -) diff --git a/tests/pyproject/priority_test/MODULE.bazel b/tests/pyproject/priority_test/MODULE.bazel deleted file mode 100644 index 8754c77f37..0000000000 --- a/tests/pyproject/priority_test/MODULE.bazel +++ /dev/null @@ -1,16 +0,0 @@ -"""Test that pyproject.toml takes priority over other version sources.""" - -module(name = "priority_test") - -bazel_dep(name = "rules_python", version = "") -local_path_override( - module_name = "rules_python", - path = "../../..", -) - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.defaults( - pyproject_toml = "//:pyproject.toml", - python_version_file = "//:.python-version", -) -use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/priority_test/pyproject.toml b/tests/pyproject/priority_test/pyproject.toml deleted file mode 100644 index 8268f6f5d7..0000000000 --- a/tests/pyproject/priority_test/pyproject.toml +++ /dev/null @@ -1,4 +0,0 @@ -[project] -name = "priority-test" -dynamic = ["version"] -requires-python = "==3.11.7" diff --git a/tests/pyproject/priority_test/test_priority.py b/tests/pyproject/priority_test/test_priority.py deleted file mode 100644 index 2dff7e858c..0000000000 --- a/tests/pyproject/priority_test/test_priority.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test that pyproject.toml takes priority over .python-version.""" - -import sys -import unittest - - -class PriorityTest(unittest.TestCase): - def test_pyproject_wins_over_python_version_file(self): - """ - Verify pyproject.toml (3.11.7) takes priority over .python-version (3.12.0). - """ - version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - self.assertEqual( - version, - "3.11.7", - "pyproject.toml should take priority over .python-version file", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pyproject/python_toolchain_test/BUILD.bazel b/tests/pyproject/python_toolchain_test/BUILD.bazel deleted file mode 100644 index 5dd2afcbf3..0000000000 --- a/tests/pyproject/python_toolchain_test/BUILD.bazel +++ /dev/null @@ -1,7 +0,0 @@ -load("@rules_python//python:defs.bzl", "py_test") - -py_test( - name = "test_version", - srcs = ["test_version.py"], - main = "test_version.py", -) diff --git a/tests/pyproject/python_toolchain_test/MODULE.bazel b/tests/pyproject/python_toolchain_test/MODULE.bazel deleted file mode 100644 index bcf9fc2069..0000000000 --- a/tests/pyproject/python_toolchain_test/MODULE.bazel +++ /dev/null @@ -1,13 +0,0 @@ -"""Test that pyproject.toml sets Python toolchain version.""" - -module(name = "python_toolchain_test") - -bazel_dep(name = "rules_python", version = "") -local_path_override( - module_name = "rules_python", - path = "../../..", -) - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.defaults(pyproject_toml = "//:pyproject.toml") -use_repo(python, "python_version_from_pyproject", "python_versions", "pythons_hub") diff --git a/tests/pyproject/python_toolchain_test/pyproject.toml b/tests/pyproject/python_toolchain_test/pyproject.toml deleted file mode 100644 index aea4119799..0000000000 --- a/tests/pyproject/python_toolchain_test/pyproject.toml +++ /dev/null @@ -1,4 +0,0 @@ -[project] -name = "python-toolchain-test" -dynamic = ["version"] -requires-python = "==3.11.7" diff --git a/tests/pyproject/python_toolchain_test/test_version.py b/tests/pyproject/python_toolchain_test/test_version.py deleted file mode 100644 index 09ef166f6c..0000000000 --- a/tests/pyproject/python_toolchain_test/test_version.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test that Python version matches pyproject.toml requires-python.""" - -import sys -import unittest - - -class PythonVersionTest(unittest.TestCase): - def test_python_version_from_pyproject(self): - """Verify we're running Python 3.11.7 as specified in pyproject.toml.""" - version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - self.assertEqual( - version, - "3.11.7", - f"Expected Python 3.11.7 from pyproject.toml, got {version}", - ) - - -if __name__ == "__main__": - unittest.main() From d8769f8797b46077e2322c005fe7637a226558e3 Mon Sep 17 00:00:00 2001 From: Jan Winkler Date: Thu, 12 Feb 2026 11:02:58 +0100 Subject: [PATCH 7/9] Use toml2json --- python/private/BUILD.bazel | 1 - python/private/pypi/extension.bzl | 4 ++- python/private/pypi/pypi_repo_utils.bzl | 2 +- python/private/pyproject_repo.bzl | 40 +++++++++++++++++-------- python/private/pyproject_utils.bzl | 40 +++++++++++++++++++++---- python/private/python.bzl | 3 +- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 0ef93813ae..66bbf60888 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -31,7 +31,6 @@ licenses(["notice"]) exports_files([ "runtime_env_toolchain_interpreter.sh", - "pyproject_version_extractor.py", ]) filegroup( diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 39188169ea..9ac65797fa 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -747,8 +747,10 @@ code will be re-evaluated when any of files in the default changes. "_toml2json": attr.label( default = "//tools/private/toml2json:toml2json.py", ), + # TODO: Set a proper default once interpreter bootstrapping is resolved. + # See https://github.com/bazel-contrib/rules_python/pull/3557 "_tool_python_interpreter": attr.label( - default = "@python_3_14_host//:BUILD.bazel", + default = None, ), }, **ATTRS) attrs.update(AUTH_ATTRS) diff --git a/python/private/pypi/pypi_repo_utils.bzl b/python/private/pypi/pypi_repo_utils.bzl index c052dd3321..8acea48156 100644 --- a/python/private/pypi/pypi_repo_utils.bzl +++ b/python/private/pypi/pypi_repo_utils.bzl @@ -188,7 +188,7 @@ def _run_toml2json(mrctx, toml_label, attr, logger = None): fail("toml file does not exist: {} (from label {})".format(toml_path, toml_label)) # Use the shared toml2json tool - toml2json_tool = mrctx.path(ctx.attr._toml2json) + toml2json_tool = mrctx.path(attr._toml2json) stdout = _execute_checked_stdout( mrctx, diff --git a/python/private/pyproject_repo.bzl b/python/private/pyproject_repo.bzl index e62ec2b4e4..f54e0aa80e 100644 --- a/python/private/pyproject_repo.bzl +++ b/python/private/pyproject_repo.bzl @@ -1,27 +1,45 @@ """Repository rule to expose Python version from pyproject.toml.""" -_EXTRACTOR_SCRIPT = Label("//python/private:pyproject_version_extractor.py") +_TOML2JSON = Label("//tools/private/toml2json:toml2json.py") + +def _parse_requires_python(requires_python): + """Parse and validate the requires-python field.""" + if not requires_python.startswith("=="): + fail("requires-python must use '==' for exact version, got: {}".format(requires_python)) + + bare_version = requires_python[2:].strip() + parts = bare_version.split(".") + if len(parts) != 3: + fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) + for part in parts: + if not part.isdigit(): + fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) + + return bare_version def _pyproject_version_repo_impl(rctx): """Create a repository that exports PYTHON_VERSION from pyproject.toml.""" pyproject_path = rctx.path(rctx.attr.pyproject_toml) rctx.read(pyproject_path, watch = "yes") - # Use the shared extractor script - extractor = rctx.path(_EXTRACTOR_SCRIPT) + toml2json = rctx.path(_TOML2JSON) result = rctx.execute([ "python3", - str(extractor), + str(toml2json), str(pyproject_path), ]) if result.return_code != 0: - fail("Failed to read Python version from pyproject.toml: " + result.stderr) + fail("Failed to parse pyproject.toml: " + result.stderr) + + data = json.decode(result.stdout) + requires_python = data.get("project", {}).get("requires-python") + if not requires_python: + fail("pyproject.toml must contain [project] requires-python field") - version = result.stdout.strip() + version = _parse_requires_python(requires_python) - # Create a .bzl file that exports the version - rctx.file("version.bzl", """ + rctx.file("version.bzl", """\ \"\"\"Python version from pyproject.toml. This file is automatically generated. Do not edit. @@ -30,7 +48,7 @@ This file is automatically generated. Do not edit. PYTHON_VERSION = "{version}" """.format(version = version)) - rctx.file("BUILD.bazel", """ + rctx.file("BUILD.bazel", """\ # Automatically generated from pyproject.toml exports_files(["version.bzl"]) """) @@ -40,15 +58,13 @@ pyproject_version_repo = repository_rule( attrs = { "pyproject_toml": attr.label( mandatory = True, - allow_single_file = True, doc = "Label pointing to pyproject.toml file.", ), }, doc = """Repository rule that reads Python version from pyproject.toml. This rule creates a repository with a `version.bzl` file that exports -`PYTHON_VERSION` constant. This allows BUILD files to import the Python -version without hardcoding it. +`PYTHON_VERSION` constant. Example: ```python diff --git a/python/private/pyproject_utils.bzl b/python/private/pyproject_utils.bzl index eaf03332e2..6b79870c47 100644 --- a/python/private/pyproject_utils.bzl +++ b/python/private/pyproject_utils.bzl @@ -1,6 +1,30 @@ """Utilities for reading Python version from pyproject.toml.""" -_EXTRACTOR_SCRIPT = Label("//python/private:pyproject_version_extractor.py") +_TOML2JSON = Label("//tools/private/toml2json:toml2json.py") + +def _parse_requires_python(requires_python): + """Parse and validate the requires-python field. + + Args: + requires_python: The raw requires-python string from pyproject.toml. + + Returns: + The bare version string (e.g. "3.13.9"). + """ + if not requires_python.startswith("=="): + fail("requires-python must use '==' for exact version, got: {}".format(requires_python)) + + bare_version = requires_python[2:].strip() + + # Validate X.Y.Z format + parts = bare_version.split(".") + if len(parts) != 3: + fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) + for part in parts: + if not part.isdigit(): + fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) + + return bare_version def read_pyproject_version(module_ctx, pyproject_label, logger = None): """Reads Python version from pyproject.toml if requested. @@ -19,18 +43,22 @@ def read_pyproject_version(module_ctx, pyproject_label, logger = None): pyproject_path = module_ctx.path(pyproject_label) module_ctx.read(pyproject_path, watch = "yes") - # Use the shared extractor script - extractor = module_ctx.path(_EXTRACTOR_SCRIPT) + toml2json = module_ctx.path(_TOML2JSON) result = module_ctx.execute([ "python3", - str(extractor), + str(toml2json), str(pyproject_path), ]) if result.return_code != 0: - fail("Failed to read Python version from pyproject.toml: " + result.stderr) + fail("Failed to parse pyproject.toml: " + result.stderr) + + data = json.decode(result.stdout) + requires_python = data.get("project", {}).get("requires-python") + if not requires_python: + fail("pyproject.toml must contain [project] requires-python field") - version = result.stdout.strip() + version = _parse_requires_python(requires_python) if logger: logger.info(lambda: "Read Python version {} from {}".format(version, pyproject_label)) diff --git a/python/private/python.bzl b/python/private/python.bzl index f6394f0231..24c38aae15 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -1023,13 +1023,12 @@ _defaults = tag_class( attrs = { "pyproject_toml": attr.label( mandatory = False, - allow_single_file = True, doc = """\ Label pointing to pyproject.toml file to read the default Python version from. When specified, reads the `requires-python` field from pyproject.toml. The version must be specified as `==X.Y.Z` (exact version with full semver). -:::{versionadded} 1.8.0 +:::{versionadded} VERSION_NEXT_FEATURE ::: """, ), From 56b98219d7b24722ed8924059a489d8ef835c7c1 Mon Sep 17 00:00:00 2001 From: Jan Winkler Date: Thu, 12 Feb 2026 11:21:16 +0100 Subject: [PATCH 8/9] Reformat --- .bazelrc.deleted_packages | 4 ---- tests/tools/private/toml2json/BUILD.bazel | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index 99cd0e3dea..2d8a8075fa 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -45,7 +45,3 @@ common --deleted_packages=tests/modules/other/nspkg_single common --deleted_packages=tests/modules/other/simple_v1 common --deleted_packages=tests/modules/other/simple_v2 common --deleted_packages=tests/modules/other/with_external_data -common --deleted_packages=tests/pyproject/compile_requirements_test -common --deleted_packages=tests/pyproject/pip_integration_test -common --deleted_packages=tests/pyproject/priority_test -common --deleted_packages=tests/pyproject/python_toolchain_test diff --git a/tests/tools/private/toml2json/BUILD.bazel b/tests/tools/private/toml2json/BUILD.bazel index 8c4e94df7d..e8830f0030 100644 --- a/tests/tools/private/toml2json/BUILD.bazel +++ b/tests/tools/private/toml2json/BUILD.bazel @@ -7,4 +7,4 @@ py_test( deps = [ "//tools/private/toml2json", ], -) \ No newline at end of file +) From 653998beca067cf0ee73340b3eb8a35a7f1aa724 Mon Sep 17 00:00:00 2001 From: Jan Winkler Date: Thu, 12 Feb 2026 11:27:35 +0100 Subject: [PATCH 9/9] Remove accidentally commited file --- 2 | 1460 ------------------------------------------------------------- 1 file changed, 1460 deletions(-) delete mode 100644 2 diff --git a/2 b/2 deleted file mode 100644 index 3266c831b8..0000000000 --- a/2 +++ /dev/null @@ -1,1460 +0,0 @@ -# Copyright 2023 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. - -"Python toolchain module extensions for use with bzlmod." - -load("@bazel_features//:features.bzl", "bazel_features") -load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") -load(":auth.bzl", "AUTH_ATTRS") -load(":full_version.bzl", "full_version") -load(":platform_info.bzl", "platform_info") -load(":pyproject_repo.bzl", "pyproject_version_repo") -load(":pyproject_utils.bzl", "read_pyproject_version") -load(":python_register_toolchains.bzl", "python_register_toolchains") -load(":pythons_hub.bzl", "hub_repo") -load(":repo_utils.bzl", "repo_utils") -load( - ":toolchains_repo.bzl", - "host_compatible_python_repo", - "multi_toolchain_aliases", - "sorted_host_platform_names", - "sorted_host_platforms", -) -load(":version.bzl", "version") - -def parse_modules(*, module_ctx, logger, _fail = fail): - """Parse the modules and return a struct for registrations. - - Args: - module_ctx: {type}`module_ctx` module context. - logger: {type}`repo_utils.logger` A logger to use. - _fail: {type}`function` the failure function, mainly for testing. - - Returns: - A struct with the following attributes: - * `toolchains`: {type}`list[ToolchainConfig]` The list of toolchains to - register. The last element is special and is treated as the default - toolchain. - * `config`: Various toolchain config, see `_get_toolchain_config`. - * `debug_info`: {type}`None | dict` extra information to be passed - to the debug repo. - * `platforms`: {type}`dict[str, platform_info]` of the base set of - platforms toolchains should be created for, if possible. - - ToolchainConfig struct: - * python_version: str, full python version string - * name: str, the base toolchain name, e.g., "python_3_10", no - platform suffix. - * register_coverage_tool: bool - """ - if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": - debug_info = { - "toolchains_registered": [], - } - else: - debug_info = None - - # 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 or Major.Minor.Patch to the toolchain_info struct - global_toolchain_versions = {} - - config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) - - default_python_version = _compute_default_python_version(module_ctx) - - seen_versions = {} - for mod in module_ctx.modules: - module_toolchain_versions = [] - toolchain_attr_structs = _create_toolchain_attr_structs( - mod = mod, - seen_versions = seen_versions, - config = config, - module_ctx = module_ctx, - logger = logger, - ) - - for toolchain_attr in toolchain_attr_structs: - toolchain_version = toolchain_attr.python_version - toolchain_name = "python_" + toolchain_version.replace(".", "_") - - # Duplicate versions within a module indicate a misconfigured module. - if toolchain_version in module_toolchain_versions: - _fail_duplicate_module_toolchain_version(toolchain_version, mod.name) - module_toolchain_versions.append(toolchain_version) - - if mod.is_root: - # Only the root module and rules_python are allowed to specify the default - # toolchain for a couple reasons: - # * It prevents submodules from specifying different defaults and only - # one of them winning. - # * rules_python needs to set a soft default in case the root module doesn't, - # e.g. if the root module doesn't use Python itself. - # * The root module is allowed to override the rules_python default. - is_default = default_python_version == toolchain_version - - elif mod.name == "rules_python" and not default_toolchain: - # This branch handles when the root module doesn't declare a - # Python toolchain - is_default = default_python_version == toolchain_version - else: - is_default = False - - if is_default and default_toolchain != None: - _fail_multiple_default_toolchains_chosen( - first = default_toolchain.name, - second = toolchain_name, - ) - - # Ignore version collisions in the global scope because there isn't - # much else that can be done. Modules don't know and can't control - # what other modules do, so the first in the dependency graph wins. - if toolchain_version in global_toolchain_versions: - # If the python version is explicitly provided by the root - # module, they should not be warned for choosing the same - # version that rules_python provides as default. - first = global_toolchain_versions[toolchain_version] - if mod.name != "rules_python" or not first.module.is_root: - # The warning can be enabled by setting the verbosity: - # env RULES_PYTHON_REPO_DEBUG_VERBOSITY=INFO bazel build //... - _warn_duplicate_global_toolchain_version( - toolchain_version, - first = first, - second_toolchain_name = toolchain_name, - second_module_name = mod.name, - logger = logger, - ) - toolchain_info = None - else: - toolchain_info = struct( - python_version = toolchain_attr.python_version, - name = toolchain_name, - register_coverage_tool = toolchain_attr.configure_coverage_tool, - module = struct(name = mod.name, is_root = mod.is_root), - ) - global_toolchain_versions[toolchain_version] = toolchain_info - if debug_info: - debug_info["toolchains_registered"].append({ - "module": {"is_root": mod.is_root, "name": mod.name}, - "name": toolchain_name, - }) - - if is_default: - # This toolchain is setting the default, but the actual - # registration was performed previously, by a different module. - if toolchain_info == None: - default_toolchain = global_toolchain_versions[toolchain_version] - - # Remove it because later code will add it at the end to - # ensure it is last in the list. - toolchains.remove(default_toolchain) - else: - default_toolchain = toolchain_info - elif toolchain_info: - toolchains.append(toolchain_info) - - # A default toolchain is required so that the non-version-specific rules - # are able to match a toolchain. - if default_toolchain == None: - fail("No default Python toolchain configured. Is rules_python missing `python.defaults()`?") - elif default_toolchain.python_version not in global_toolchain_versions: - fail('Default version "{python_version}" selected by module ' + - '"{module_name}", but no toolchain with that version registered'.format( - python_version = default_toolchain.python_version, - module_name = default_toolchain.module.name, - )) - - # The last toolchain in the BUILD file is set as the default - # toolchain. We need the default last. - toolchains.append(default_toolchain) - - # sort the toolchains so that the toolchain versions that are in the - # `minor_mapping` are coming first. This ensures that `python_version = - # "3.X"` transitions work as expected. - minor_version_toolchains = [] - other_toolchains = [] - minor_mapping = list(config.minor_mapping.values()) - for t in toolchains: - # FIXME @aignas 2025-04-04: How can we unit test that this ordering is - # consistent with what would actually work? - if config.minor_mapping.get(t.python_version, t.python_version) in minor_mapping: - minor_version_toolchains.append(t) - else: - other_toolchains.append(t) - toolchains = minor_version_toolchains + other_toolchains - - return struct( - config = config, - debug_info = debug_info, - default_python_version = default_toolchain.python_version, - toolchains = [ - struct( - python_version = t.python_version, - name = t.name, - register_coverage_tool = t.register_coverage_tool, - ) - for t in toolchains - ], - ) - -def _python_impl(module_ctx): - logger = repo_utils.logger(module_ctx, "python") - py = parse_modules(module_ctx = module_ctx, logger = logger) - - # Create pyproject version repo if pyproject.toml is used - created_pyproject_repo = False - for mod in module_ctx.modules: - if mod.is_root: - for tag in mod.tags.defaults: - if tag.pyproject_toml: - pyproject_version_repo( - name = "python_version_from_pyproject", - pyproject_toml = tag.pyproject_toml, - ) - created_pyproject_repo = True - break - break - - # Host compatible runtime repos - # dict[str version, struct] where struct has: - # * full_python_version: str - # * platform: platform_info struct - # * platform_name: str platform name - # * impl_repo_name: str repo name of the runtime's python_repository() repo - all_host_compatible_impls = {} - - # Host compatible repos that still need to be created because, when - # creating the actual runtime repo, there wasn't a host-compatible - # variant defined for it. - # dict[str reponame, struct] where struct has: - # * compatible_version: str, e.g. 3.10 or 3.10.1. The version the host - # repo should be compatible with - # * full_python_version: str, e.g. 3.10.1, the full python version of - # the toolchain that still needs a host repo created. - needed_host_repos = {} - - # list of structs; see inline struct call within the loop below. - toolchain_impls = [] - - # list[str] of the repo names for host compatible repos - all_host_compatible_repo_names = [] - - # Create the underlying python_repository repos that contain the - # python runtimes and their toolchain implementation definitions. - for i, toolchain_info in enumerate(py.toolchains): - is_last = (i + 1) == len(py.toolchains) - - # Ensure that we pass the full version here. - full_python_version = full_version( - version = toolchain_info.python_version, - minor_mapping = py.config.minor_mapping, - fail_on_err = False, - ) - if not full_python_version: - logger.info(lambda: ( - "The actual toolchain for python_version '{version}' " + - "has not been registered, but was requested, please configure a toolchain " + - "to be actually downloaded and setup" - ).format( - version = toolchain_info.python_version, - )) - continue - - kwargs = { - "python_version": full_python_version, - "register_coverage_tool": toolchain_info.register_coverage_tool, - } - - # Allow overrides per python version - kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) - kwargs.update(py.config.kwargs.get(full_python_version, {})) - kwargs.update(py.config.default) - register_result = python_register_toolchains( - name = toolchain_info.name, - _internal_bzlmod_toolchain_call = True, - **kwargs - ) - if not register_result.impl_repos: - continue - - host_platforms = {} - for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): - toolchain_impls.append(struct( - # str: The base name to use for the toolchain() target - name = repo_name, - # str: The repo name the toolchain() target points to. - impl_repo_name = repo_name, - # str: platform key in the passed-in platforms dict - platform_name = platform_name, - # struct: platform_info() struct - platform = platform_info, - # str: Major.Minor.Micro python version - full_python_version = full_python_version, - # bool: whether to implicitly add the python version constraint - # to the toolchain's target_settings. - # The last toolchain is the default; it can't have version constraints - set_python_version_constraint = is_last, - )) - if _is_compatible_with_host(module_ctx, platform_info): - host_compat_entry = struct( - full_python_version = full_python_version, - platform = platform_info, - platform_name = platform_name, - impl_repo_name = repo_name, - ) - host_platforms[platform_name] = host_compat_entry - all_host_compatible_impls.setdefault(full_python_version, []).append( - host_compat_entry, - ) - parsed_version = version.parse(full_python_version) - all_host_compatible_impls.setdefault( - "{}.{}".format(*parsed_version.release[0:2]), - [], - ).append(host_compat_entry) - - host_repo_name = toolchain_info.name + "_host" - if host_platforms: - all_host_compatible_repo_names.append(host_repo_name) - host_platforms = sorted_host_platforms(host_platforms) - entries = host_platforms.values() - host_compatible_python_repo( - name = host_repo_name, - base_name = host_repo_name, - # NOTE: Order matters. The first found to be compatible is - # (usually) used. - platforms = host_platforms.keys(), - os_names = {str(i): e.platform.os_name for i, e in enumerate(entries)}, - arch_names = {str(i): e.platform.arch for i, e in enumerate(entries)}, - python_versions = {str(i): e.full_python_version for i, e in enumerate(entries)}, - impl_repo_names = {str(i): e.impl_repo_name for i, e in enumerate(entries)}, - ) - else: - needed_host_repos[host_repo_name] = struct( - compatible_version = toolchain_info.python_version, - full_python_version = full_python_version, - ) - - if needed_host_repos: - for key, entries in all_host_compatible_impls.items(): - all_host_compatible_impls[key] = sorted( - entries, - reverse = True, - key = lambda e: version.key(version.parse(e.full_python_version)), - ) - - for host_repo_name, info in needed_host_repos.items(): - choices = [] - if info.compatible_version not in all_host_compatible_impls: - logger.warn("No host compatible runtime found compatible with version {}".format(info.compatible_version)) - continue - - choices = all_host_compatible_impls[info.compatible_version] - platform_keys = [ - # We have to prepend the offset because the same platform - # name might occur across different versions - "{}_{}".format(i, entry.platform_name) - for i, entry in enumerate(choices) - ] - platform_keys = sorted_host_platform_names(platform_keys) - - all_host_compatible_repo_names.append(host_repo_name) - host_compatible_python_repo( - name = host_repo_name, - base_name = host_repo_name, - platforms = platform_keys, - impl_repo_names = { - str(i): entry.impl_repo_name - for i, entry in enumerate(choices) - }, - os_names = {str(i): entry.platform.os_name for i, entry in enumerate(choices)}, - arch_names = {str(i): entry.platform.arch for i, entry in enumerate(choices)}, - python_versions = {str(i): entry.full_python_version for i, entry in enumerate(choices)}, - ) - - # list[str] The infix to use for the resulting toolchain() `name` arg. - toolchain_names = [] - - # dict[str i, str repo]; where repo is the full repo name - # ("python_3_10_unknown-linux-x86_64") for the toolchain - # i corresponds to index `i` in toolchain_names - toolchain_repo_names = {} - - # dict[str i, list[str] constraints]; where constraints is a list - # of labels for target_compatible_with - # i corresponds to index `i` in toolchain_names - toolchain_tcw_map = {} - - # dict[str i, list[str] settings]; where settings is a list - # of labels for target_settings - # i corresponds to index `i` in toolchain_names - toolchain_ts_map = {} - - # dict[str i, str set_constraint]; where set_constraint is the string - # "True" or "False". - # i corresponds to index `i` in toolchain_names - toolchain_set_python_version_constraints = {} - - # dict[str i, str python_version]; where python_version is the full - # python version ("3.4.5"). - toolchain_python_versions = {} - - # dict[str i, str platform_key]; where platform_key is the key within - # the PLATFORMS global for this toolchain - toolchain_platform_keys = {} - - # Split the toolchain info into separate objects so they can be passed onto - # the repository rule. - for entry in toolchain_impls: - key = str(len(toolchain_names)) - - toolchain_names.append(entry.name) - toolchain_repo_names[key] = entry.impl_repo_name - toolchain_tcw_map[key] = entry.platform.compatible_with - - # The target_settings attribute may not be present for users - # patching python/versions.bzl. - toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) - toolchain_platform_keys[key] = entry.platform_name - toolchain_python_versions[key] = entry.full_python_version - - # Repo rules can't accept dict[str, bool], so encode them as a string value. - toolchain_set_python_version_constraints[key] = ( - "True" if entry.set_python_version_constraint else "False" - ) - - hub_repo( - name = "pythons_hub", - toolchain_names = toolchain_names, - toolchain_repo_names = toolchain_repo_names, - toolchain_target_compatible_with_map = toolchain_tcw_map, - toolchain_target_settings_map = toolchain_ts_map, - toolchain_platform_keys = toolchain_platform_keys, - toolchain_python_versions = toolchain_python_versions, - toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, - host_compatible_repo_names = sorted(all_host_compatible_repo_names), - default_python_version = py.default_python_version, - minor_mapping = py.config.minor_mapping, - python_versions = list(py.config.default["tool_versions"].keys()), - ) - - # This is require in order to support multiple version py_test - # and py_binary - multi_toolchain_aliases( - name = "python_versions", - python_versions = { - toolchain.python_version: toolchain.name - for toolchain in py.toolchains - }, - ) - - if py.debug_info != None: - _debug_repo( - name = "rules_python_bzlmod_debug", - debug_info = json.encode_indent(py.debug_info), - ) - - if bazel_features.external_deps.extension_metadata_has_reproducible: - # Build the list of direct dependencies - root_direct_deps = ["pythons_hub", "python_versions"] - if created_pyproject_repo: - root_direct_deps.append("python_version_from_pyproject") - - return module_ctx.extension_metadata( - root_module_direct_deps = root_direct_deps, - root_module_direct_dev_deps = [], - reproducible = True, - ) - else: - return None - -def _is_compatible_with_host(mctx, platform_info): - os_name = repo_utils.get_platforms_os_name(mctx) - cpu_name = repo_utils.get_platforms_cpu_name(mctx) - return platform_info.os_name == os_name and platform_info.arch == cpu_name - -def _one_or_the_same(first, second, *, onerror = None): - if not first: - return second - if not second or second == first: - return first - if onerror: - return onerror(first, second) - else: - fail("Unique value needed, got both '{}' and '{}', which are different".format( - first, - second, - )) - -def _fail_duplicate_module_toolchain_version(version, module): - fail(("Duplicate module toolchain version: module '{module}' attempted " + - "to use version '{version}' multiple times in itself").format( - version = version, - module = 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}' " + - "already registered Python version {version} and has precedence." - ).format( - first_toolchain = first.name, - first_module = first.module.name, - second_module = second_module_name, - second_toolchain = second_toolchain_name, - version = version, - )) - -def _fail_multiple_defaults_python_version(first, second): - fail(("Multiple python_version entries in defaults: " + - "First default was python_version '{first}'. " + - "Second was python_version '{second}'").format( - first = first, - second = second, - )) - -def _fail_multiple_defaults_python_version_file(first, second): - fail(("Multiple python_version_file entries in defaults: " + - "First default was python_version_file '{first}'. " + - "Second was python_version_file '{second}'").format( - first = first, - second = second, - )) - -def _fail_multiple_defaults_python_version_env(first, second): - fail(("Multiple python_version_env entries in defaults: " + - "First default was python_version_env '{first}'. " + - "Second was python_version_env '{second}'").format( - first = first, - second = second, - )) - -def _fail_multiple_default_toolchains_chosen(first, second): - fail(("Multiple default toolchains: only one toolchain " + - "can be chosen as a default. First default " + - "was toolchain '{first}'. Second was '{second}'").format( - first = first, - second = second, - )) - -def _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs): - fail(("Multiple default toolchains: only one toolchain " + - "can have is_default=True.\n" + - "Module '{module}' contains {count} toolchains with " + - "is_default=True: {versions}").format( - module = mod.name, - count = len(toolchain_attrs), - versions = ", ".join(sorted([v.python_version for v in toolchain_attrs])), - )) - -def _validate_version(version_str, *, _fail = fail): - v = version.parse(version_str, strict = True, _fail = _fail) - if v == None: - # Only reachable in tests - return False - - if len(v.release) < 3: - _fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string)) - return False - - return True - -def _process_single_version_overrides(*, tag, _fail = fail, default): - if not _validate_version(tag.python_version, _fail = _fail): - return - - available_versions = default["tool_versions"] - kwargs = default.setdefault("kwargs", {}) - - if tag.sha256 or tag.urls: - if not (tag.sha256 and tag.urls): - _fail("Both `sha256` and `urls` overrides need to be provided together") - return - - for platform in (tag.sha256 or []): - if platform not in default["platforms"]: - _fail("The platform must be one of {allowed} but got '{got}'".format( - allowed = sorted(default["platforms"]), - got = platform, - )) - return - - 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 - -def _process_single_version_platform_overrides(*, tag, _fail = fail, default): - if not _validate_version(tag.python_version, _fail = _fail): - return - - available_versions = default["tool_versions"] - - 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)) - return - 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 - - # If platform is customized, or doesn't exist, (re)define one. - if ((tag.target_compatible_with or tag.target_settings or tag.os_name or tag.arch) or - tag.platform not in default["platforms"]): - os_name = tag.os_name - arch = tag.arch - - if not tag.target_compatible_with: - target_compatible_with = [] - if os_name: - target_compatible_with.append("@platforms//os:{}".format( - repo_utils.get_platforms_os_name(os_name), - )) - if arch: - target_compatible_with.append("@platforms//cpu:{}".format( - repo_utils.get_platforms_cpu_name(arch), - )) - else: - target_compatible_with = tag.target_compatible_with - - # For lack of a better option, give a bogus value. It only affects - # if the runtime is considered host-compatible. - if not os_name: - os_name = "UNKNOWN_CUSTOM_OS" - if not arch: - arch = "UNKNOWN_CUSTOM_ARCH" - - # Move the override earlier in the ordering -- the platform key ordering - # becomes the toolchain ordering within the version. This allows the - # override to have a superset of constraints from a regular runtimes - # (e.g. same platform, but with a custom flag required). - override_first = { - tag.platform: platform_info( - compatible_with = target_compatible_with, - target_settings = tag.target_settings, - os_name = os_name, - arch = arch, - ), - } - for key, value in default["platforms"].items(): - # Don't replace our override with the old value - if key in override_first: - continue - override_first[key] = value - - default["platforms"] = override_first - -def _process_global_overrides(*, tag, default, _fail = fail): - if tag.available_python_versions: - available_versions = default["tool_versions"] - all_versions = dict(available_versions) - available_versions.clear() - for v in tag.available_python_versions: - if v not in all_versions: - _fail("unknown version '{}', known versions are: {}".format( - v, - sorted(all_versions), - )) - return - - available_versions[v] = all_versions[v] - - if tag.minor_mapping: - for minor_version, full_version in tag.minor_mapping.items(): - parsed = version.parse(minor_version, strict = True, _fail = _fail) - if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local: - fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string)) - - # Ensure that the version is valid - version.parse(full_version, strict = True, _fail = _fail) - - default["minor_mapping"] = tag.minor_mapping - - forwarded_attrs = sorted(AUTH_ATTRS) + [ - "base_url", - "register_all_versions", - ] - for key in forwarded_attrs: - if getattr(tag, key, None): - default[key] = getattr(tag, key) - -def _override_defaults(*overrides, modules, _fail = fail, default): - mod = modules[0] if modules else None - if not mod or not mod.is_root: - return - - overriden_keys = [] - - for override in overrides: - for tag in getattr(mod.tags, override.name): - key = override.key(tag) - if key not in overriden_keys: - overriden_keys.append(key) - elif key: - _fail("Only a single 'python.{}' can be present for '{}'".format(override.name, key)) - return - else: - _fail("Only a single 'python.{}' can be present".format(override.name)) - return - - override.fn(tag = tag, _fail = _fail, default = default) - -def _get_toolchain_config(*, modules, _fail = fail): - """Computes the configs for toolchains. - - Args: - modules: The modules from module_ctx - _fail: Function to call for failing; only used for testing. - - Returns: - A struct with the following: - * `kwargs`: {type}`dict[str, dict[str, object]` custom kwargs to pass to - `python_register_toolchains`, keyed by python version. - The first key is either a Major.Minor or Major.Minor.Patch - string. - * `minor_mapping`: {type}`dict[str, str]` the mapping of Major.Minor - to Major.Minor.Patch. - * `default`: {type}`dict[str, object]` of kwargs passed along to - `python_register_toolchains`. These keys take final precedence. - * `register_all_versions`: {type}`bool` whether all known versions - should be registered. - """ - - # Items that can be overridden - available_versions = {} - for py_version, item in TOOL_VERSIONS.items(): - available_versions[py_version] = {} - available_versions[py_version]["sha256"] = dict(item["sha256"]) - platforms = item["sha256"].keys() - - strip_prefix = item["strip_prefix"] - if type(strip_prefix) == type(""): - available_versions[py_version]["strip_prefix"] = { - platform: strip_prefix - for platform in platforms - } - else: - available_versions[py_version]["strip_prefix"] = dict(strip_prefix) - url = item["url"] - if type(url) == type(""): - available_versions[py_version]["url"] = { - platform: url - for platform in platforms - } - else: - available_versions[py_version]["url"] = dict(url) - - default = { - "base_url": DEFAULT_RELEASE_BASE_URL, - "platforms": dict(PLATFORMS), # Copy so it's mutable. - "tool_versions": available_versions, - } - - _override_defaults( - # First override by single version, because the sha256 will replace - # anything that has been there before. - struct( - name = "single_version_override", - key = lambda t: t.python_version, - fn = _process_single_version_overrides, - ), - # Then override particular platform entries if they need to be overridden. - struct( - name = "single_version_platform_override", - key = lambda t: (t.python_version, t.platform), - fn = _process_single_version_platform_overrides, - ), - # Then finally add global args and remove the unnecessary toolchains. - # This ensures that we can do further validations when removing. - struct( - name = "override", - key = lambda t: None, - fn = _process_global_overrides, - ), - modules = modules, - default = default, - _fail = _fail, - ) - - register_all_versions = default.pop("register_all_versions", False) - kwargs = default.pop("kwargs", {}) - - versions = {} - for version_string in available_versions: - v = version.parse(version_string, strict = True) - versions.setdefault( - "{}.{}".format(v.release[0], v.release[1]), - [], - ).append((version.key(v), v.string)) - - minor_mapping = { - major_minor: max(subset)[1] - for major_minor, subset in versions.items() - } - - # The following ensures that all of the versions will be present in the minor_mapping - minor_mapping_overrides = default.pop("minor_mapping", {}) - for major_minor, full in minor_mapping_overrides.items(): - minor_mapping[major_minor] = full - - return struct( - kwargs = kwargs, - minor_mapping = minor_mapping, - default = default, - register_all_versions = register_all_versions, - ) - -def _compute_default_python_version(mctx): - default_python_version = None - for mod in mctx.modules: - # Only the root module and rules_python are allowed to specify the default - # toolchain for a couple reasons: - # * It prevents submodules from specifying different defaults and only - # one of them winning. - # * rules_python needs to set a soft default in case the root module doesn't, - # e.g. if the root module doesn't use Python itself. - # * The root module is allowed to override the rules_python default. - if not (mod.is_root or mod.name == "rules_python"): - continue - - defaults_attr_structs = _create_defaults_attr_structs(mod = mod) - default_python_version_env = None - default_python_version_file = None - pyproject_toml_label = None - - for defaults_attr in defaults_attr_structs: - pyproject_toml_label = _one_or_the_same( - pyproject_toml_label, - defaults_attr.pyproject_toml, - onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), - ) - - default_python_version = _one_or_the_same( - default_python_version, - defaults_attr.python_version, - onerror = _fail_multiple_defaults_python_version, - ) - default_python_version_env = _one_or_the_same( - default_python_version_env, - defaults_attr.python_version_env, - onerror = _fail_multiple_defaults_python_version_env, - ) - default_python_version_file = _one_or_the_same( - default_python_version_file, - defaults_attr.python_version_file, - onerror = _fail_multiple_defaults_python_version_file, - ) - - if default_python_version_file: - default_python_version = _one_or_the_same( - default_python_version, - mctx.read(default_python_version_file, watch = "yes").strip(), - ) - if pyproject_toml_label: - pyproject_version = read_pyproject_version( - mctx, - pyproject_toml_label, - logger = None, - ) - if pyproject_version: - default_python_version = pyproject_version - if default_python_version_env: - default_python_version = mctx.getenv( - default_python_version_env, - default_python_version, - ) - - if default_python_version: - break - - # Otherwise, look at legacy python.toolchain() calls for a default - toolchain_attrs = mod.tags.toolchain - - # Convenience: if one python.toolchain() call exists, treat it as - # the default. - if len(toolchain_attrs) == 1: - default_python_version = toolchain_attrs[0].python_version - else: - sets_default = [v for v in toolchain_attrs if v.is_default] - if len(sets_default) == 1: - default_python_version = sets_default[0].python_version - elif len(sets_default) > 1: - _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs) - - if default_python_version: - break - - return default_python_version - -def _create_defaults_attr_structs(*, mod): - arg_structs = [] - - for tag in mod.tags.defaults: - arg_structs.append(_create_defaults_attr_struct(tag = tag)) - - return arg_structs - -def _create_defaults_attr_struct(*, tag): - return struct( - python_version = getattr(tag, "python_version", None), - python_version_env = getattr(tag, "python_version_env", None), - python_version_file = getattr(tag, "python_version_file", None), - pyproject_toml = getattr(tag, "pyproject_toml", None), - ) - -def _create_toolchain_attr_structs(*, mod, config, seen_versions, module_ctx, logger): - arg_structs = [] - - # Check if pyproject_toml was specified in defaults - # If so, register a toolchain for it - for tag in mod.tags.defaults: - pyproject_toml = getattr(tag, "pyproject_toml", None) - if pyproject_toml: - pyproject_version = read_pyproject_version( - module_ctx, - pyproject_toml, - logger, - ) - if pyproject_version and pyproject_version not in seen_versions: - arg_structs.append(_create_toolchain_attrs_struct( - python_version = pyproject_version, - toolchain_tag_count = 1, - )) - seen_versions[pyproject_version] = True - - 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 - - if config.register_all_versions: - arg_structs.extend([ - _create_toolchain_attrs_struct(python_version = v) - for v in config.default["tool_versions"].keys() + config.minor_mapping.keys() - if v not in seen_versions - ]) - - return arg_structs - -def _create_toolchain_attrs_struct( - *, - tag = None, - python_version = None, - toolchain_tag_count = None): - if tag and python_version: - fail("Only one of tag and python version can be specified") - if tag: - # A single toolchain is treated as the default because it's unambiguous. - is_default = tag.is_default or toolchain_tag_count == 1 - else: - is_default = False - - return struct( - is_default = is_default, - python_version = python_version if python_version else tag.python_version, - configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), - ) - -_defaults = tag_class( - doc = """Tag class to specify the default Python version.""", - attrs = { - "pyproject_toml": attr.label( - mandatory = False, - allow_single_file = True, - doc = """\ -Label pointing to pyproject.toml file to read the default Python version from. -When specified, reads the `requires-python` field from pyproject.toml. -The version must be specified as `==X.Y.Z` (exact version with full semver). - -:::{versionadded} 1.8.0 -::: -""", - ), - "python_version": attr.string( - mandatory = False, - doc = """\ -String saying what the default Python version should be. If the string -matches the {attr}`python_version` attribute of a toolchain, this -toolchain is the default version. If this attribute is set, the -{attr}`is_default` attribute of the toolchain is ignored. - -:::{versionadded} 1.4.0 -::: -""", - ), - "python_version_env": attr.string( - mandatory = False, - doc = """\ -Environment variable saying what the default Python version should be. -If the string matches the {attr}`python_version` attribute of a -toolchain, this toolchain is the default version. If this attribute is -set, the {attr}`is_default` attribute of the toolchain is ignored. - -:::{versionadded} 1.4.0 -::: -""", - ), - "python_version_file": attr.label( - mandatory = False, - allow_single_file = True, - doc = """\ -File saying what the default Python version should be. If the contents -of the file match the {attr}`python_version` attribute of a toolchain, -this toolchain is the default version. If this attribute is set, the -{attr}`is_default` attribute of the toolchain is ignored. - -:::{versionadded} 1.4.0 -::: -""", - ), - }, -) - -_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. - -:::{topic} Toolchains in the Root Module - -This class registers all toolchains in the root module. -::: - -:::{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. -::: - -:::{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. -::: - -:::{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.defaults(python_version = "3.11") -python.toolchain(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 provided by `rules_python` for the compatible toolchains.", - ), - "ignore_root_user_error": attr.bool( - default = True, - doc = """\ -:::{versionchanged} 1.8.0 -Noop, will be removed in the next major release. -::: -""", - mandatory = False, - ), - "is_default": attr.bool( - mandatory = False, - doc = """\ -Whether the toolchain is the default version. - -:::{versionchanged} 1.4.0 -This setting is ignored if the default version is set using the `defaults` -tag class (encouraged). -::: -""", - ), - "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 = { - "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 = True, - doc = """Deprecated; do not use. This attribute has no effect.""", - 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", -} -``` - -:::{versionchanged} 0.37.0 -The values in this mapping override the default values and do not replace them. -::: -""", - 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 = { - "arch": attr.string( - doc = """ -The arch (cpu) the runtime is compatible with. - -If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. - -If set, the `os_name`, `target_compatible_with` and `target_settings` attributes -should also be set. - -The values should be one of the values in `@platforms//cpu` - -:::{seealso} -Docs for [Registering custom runtimes] -::: - -:::{{versionadded}} 1.5.0 -::: -""", - ), - "coverage_tool": attr.label( - doc = """\ -The coverage tool to be used for a particular Python interpreter. This can override -`rules_python` defaults. -""", - ), - "os_name": attr.string( - doc = """ -The host OS the runtime is compatible with. - -If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. - -If set, the `os_name`, `target_compatible_with` and `target_settings` attributes -should also be set. - -The values should be one of the values in `@platforms//os` - -:::{seealso} -Docs for [Registering custom runtimes] -::: - -:::{{versionadded}} 1.5.0 -::: -""", - ), - "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, - doc = """ -The platform to override the values for, typically one of:\n -{platforms} - -Other values are allowed, in which case, `target_compatible_with`, -`target_settings`, `os_name`, and `arch` should be specified so the toolchain is -only used when appropriate. - -:::{{versionchanged}} 1.5.0 -Arbitrary platform strings allowed. -::: -""".format( - platforms = "\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", - ), - "target_compatible_with": attr.string_list( - doc = """ -The `target_compatible_with` values to use for the toolchain definition. - -If not set, then `os_name` and `arch` will be used to populate it. - -If set, `target_settings`, `os_name`, and `arch` should also be set. - -:::{seealso} -Docs for [Registering custom runtimes] -::: - -:::{{versionadded}} 1.5.0 -::: -""", - ), - "target_settings": attr.string_list( - doc = """ -The `target_setings` values to use for the toolchain definition. - -If set, `target_compatible_with`, `os_name`, and `arch` should also be set. - -:::{seealso} -Docs for [Registering custom runtimes] -::: - -:::{{versionadded}} 1.5.0 -::: -""", - ), - "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 = { - "defaults": _defaults, - "override": _override, - "single_version_override": _single_version_override, - "single_version_platform_override": _single_version_platform_override, - "toolchain": _toolchain, - }, - environ = ["RULES_PYTHON_BZLMOD_DEBUG"], -) - -_DEBUG_BUILD_CONTENT = """ -package( - default_visibility = ["//visibility:public"], -) -exports_files(["debug_info.json"]) -""" - -def _debug_repo_impl(repo_ctx): - repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT) - repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info) - -_debug_repo = repository_rule( - implementation = _debug_repo_impl, - attrs = { - "debug_info": attr.string(), - }, -)