Skip to content
Merged
40 changes: 32 additions & 8 deletions python/private/config_settings.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ If the value is missing, then the default value is being used, see documentation
# access it, but it's not intended for general public usage.
_NOT_ACTUALLY_PUBLIC = ["//visibility:public"]

def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring
def construct_config_settings(
*,
name,
default_version,
versions,
minor_mapping,
compat_lowest_version = "3.8",
documented_flags): # buildifier: disable=function-docstring
"""Create a 'python_version' config flag and construct all config settings used in rules_python.
This mainly includes the targets that are used in the toolchain and pip hub
Expand All @@ -46,6 +53,8 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping,
default_version: {type}`str` the default value for the `python_version` flag.
versions: {type}`list[str]` A list of versions to build constraint settings for.
minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions.
compat_lowest_version: {type}`str` The version that we should use as the lowest available
version for `is_python_3.X` flags.
documented_flags: {type}`list[str]` The labels of the documented settings
that affect build configuration.
"""
Expand All @@ -69,21 +78,21 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping,
)

_reverse_minor_mapping = {full: minor for minor, full in minor_mapping.items()}
for version in versions:
minor_version = _reverse_minor_mapping.get(version)
for ver in versions:
minor_version = _reverse_minor_mapping.get(ver)
if not minor_version:
native.config_setting(
name = "is_python_{}".format(version),
flag_values = {":python_version": version},
name = "is_python_{}".format(ver),
flag_values = {":python_version": ver},
visibility = ["//visibility:public"],
)
continue

# Also need to match the minor version when using
name = "is_python_{}".format(version)
name = "is_python_{}".format(ver)
native.config_setting(
name = "_" + name,
flag_values = {":python_version": version},
flag_values = {":python_version": ver},
visibility = ["//visibility:public"],
)

Expand All @@ -94,7 +103,7 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping,
selects.config_setting_group(
name = "_{}_group".format(name),
match_any = [
":_is_python_{}".format(version),
":_is_python_{}".format(ver),
":is_python_{}".format(minor_version),
],
visibility = ["//visibility:private"],
Expand All @@ -109,13 +118,28 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping,
# It's private because matching the concept of e.g. "3.8" value is done
# using the `is_python_X.Y` config setting group, which is aware of the
# minor versions that could match instead.
first_minor = None
for minor in minor_mapping.keys():
ver = version.parse(minor)
if first_minor == None or version.is_lt(ver, first_minor):
first_minor = ver

native.config_setting(
name = "is_python_{}".format(minor),
flag_values = {_PYTHON_VERSION_MAJOR_MINOR_FLAG: minor},
visibility = ["//visibility:public"],
)

# This is a compatibility layer to ensure that `select` statements don't break out right
# when the toolchains for EOL minor versions are no longer registered.
compat_lowest_version = version.parse(compat_lowest_version)
for minor in range(compat_lowest_version.release[-1], first_minor.release[-1]):
native.alias(
name = "is_python_3.{}".format(minor),
actual = "@platforms//:incompatible",
visibility = ["//visibility:public"],
)

_current_config(
name = "current_config",
build_setting_default = "",
Expand Down
5 changes: 4 additions & 1 deletion python/private/full_version.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@

"""A small helper to ensure that we are working with full versions."""

def full_version(*, version, minor_mapping):
def full_version(*, version, minor_mapping, fail_on_err = True):
"""Return a full version.
Args:
version: {type}`str` the version in `X.Y` or `X.Y.Z` format.
minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format.
fail_on_err: {type}`bool` whether to fail on error or return `None` instead.
Returns:
a full version given the version string. If the string is already a
Expand All @@ -31,6 +32,8 @@ def full_version(*, version, minor_mapping):
parts = version.split(".")
if len(parts) == 3:
return version
elif not fail_on_err:
return None
elif len(parts) == 2:
fail(
"Unknown Python version '{}', available values are: {}".format(
Expand Down
31 changes: 24 additions & 7 deletions python/private/pypi/hub_builder.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,29 @@ def _pip_parse(self, module_ctx, pip_attr):
version = python_version,
))

self._platforms[python_version] = _platforms(
python_version = python_version,
full_python_version = full_version(
version = python_version,
minor_mapping = self._minor_mapping,
fail_on_err = False,
)
if not full_python_version:
# NOTE @aignas 2025-11-18: If the python version is not present in our
# minor_mapping, then we will not register any packages and then the
# select in the hub repository will fail, which will prompt the user to
# configure the toolchain correctly and move forward.
self._logger.info(lambda: (
Comment on lines +123 to +127
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why log with a lambda?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me and @rickeylev decided to have lambda in the logger so that format calls do not cost any more than needed based on the logging level.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, does Bazel not have the "deferred formatting" that core python logging has?

logger.info("value is: %s", value)

The string formatting will not happen unless the log level is INFO or lower.

Copy link
Collaborator Author

@aignas aignas Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I can't remember if I had something like that prototyped earlier or not, maybe we should look into this. should not be too difficult and would definitely remove the need for doing those inline lambdas.

That said, the current way allows us to use the render.dict helper to print a nice dictionary, because we are passing a lambda and there will be no penalty for that.

"Ignoring pip python version '{version}' for hub " +
"'{hub}' in module '{module}' because there is no registered " +
"toolchain for it."
).format(
Comment on lines +127 to +131
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more readable as a msg variable:

msg = (
    "Ignoring pipi python ..."
    "'{hub}' in module ..."
    ...
)
self._logger.info(msg.format(...))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this as is so that it is not outside the lambda.

hub = self.name,
module = self.module_name,
version = python_version,
))
return

self._platforms[python_version] = _platforms(
python_version = full_python_version,
config = self._config,
)
_set_get_index_urls(self, pip_attr)
Expand Down Expand Up @@ -280,13 +300,10 @@ def _detect_interpreter(self, pip_attr):
path = pip_attr.python_interpreter,
)

def _platforms(*, python_version, minor_mapping, config):
def _platforms(*, python_version, config):
platforms = {}
python_version = version.parse(
full_version(
version = python_version,
minor_mapping = minor_mapping,
),
python_version,
strict = True,
)

Expand Down
11 changes: 11 additions & 0 deletions python/private/python.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,18 @@ def _python_impl(module_ctx):
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,
Expand Down
12 changes: 0 additions & 12 deletions python/versions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,6 @@ DEFAULT_RELEASE_BASE_URL = "https://github.com/astral-sh/python-build-standalone
#
# buildifier: disable=unsorted-dict-items
TOOL_VERSIONS = {
"3.8.20": {
"url": "20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz",
"sha256": {
"aarch64-apple-darwin": "2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4",
"aarch64-unknown-linux-gnu": "9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed",
"x86_64-apple-darwin": "68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb",
"x86_64-pc-windows-msvc": "41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e",
"x86_64-unknown-linux-gnu": "285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87",
},
"strip_prefix": "python",
},
"3.9.25": {
"url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz",
"sha256": {
Expand Down Expand Up @@ -872,7 +861,6 @@ TOOL_VERSIONS = {

# buildifier: disable=unsorted-dict-items
MINOR_MAPPING = {
"3.8": "3.8.20",
"3.9": "3.9.25",
"3.10": "3.10.19",
"3.11": "3.11.14",
Expand Down
71 changes: 71 additions & 0 deletions tests/python/python_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,77 @@ def _test_register_all_versions(env):

_tests.append(_test_register_all_versions)

def _test_ignore_unsupported_versions(env):
py = parse_modules(
module_ctx = _mock_mctx(
_mod(
name = "my_module",
is_root = True,
toolchain = [
_toolchain("3.11"),
_toolchain("3.12"),
_toolchain("3.13", is_default = True),
],
single_version_override = [
_single_version_override(
python_version = "3.13.0",
sha256 = {
"aarch64-unknown-linux-gnu": "deadbeef",
},
urls = ["example.org"],
),
],
single_version_platform_override = [
_single_version_platform_override(
sha256 = "deadb00f",
urls = ["something.org"],
platform = "aarch64-unknown-linux-gnu",
python_version = "3.13.99",
),
],
override = [
_override(
base_url = "",
available_python_versions = ["3.12.4", "3.13.0", "3.13.1"],
minor_mapping = {
"3.12": "3.12.4",
"3.13": "3.13.1",
},
),
],
),
),
logger = repo_utils.logger(verbosity_level = 0, name = "python"),
)

env.expect.that_str(py.default_python_version).equals("3.13")
env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([
"3.12.4",
"3.13.0",
"3.13.1",
])
env.expect.that_dict(py.config.minor_mapping).contains_exactly({
# The mapping is calculated automatically
"3.12": "3.12.4",
"3.13": "3.13.1",
})
env.expect.that_collection(py.toolchains).contains_exactly([
struct(
name = name,
python_version = version,
register_coverage_tool = False,
)
for name, version in {
# NOTE: that '3.11' wont be actually registered and present in the
# `tool_versions` above.
"python_3_11": "3.11",
"python_3_12": "3.12",
"python_3_13": "3.13",
}.items()
])

_tests.append(_test_ignore_unsupported_versions)

def _test_add_patches(env):
py = parse_modules(
module_ctx = _mock_mctx(
Expand Down