Skip to content

Commit 654b4d5

Browse files
authored
chore: remove mandatory default values for python version flag (bazel-contrib#2217)
This is a different take on bazel-contrib#2205. Summary: - Remove version constraints from the `//python/config_settings:python_version`. - Remove `is_python_config_setting` macro and use a different implementation to retain the same functionality. This in general will improve the situation on how the toolchains are registered and hopefully is a step towards resolving issues for bazel-contrib#2081.
1 parent 5a856e5 commit 654b4d5

File tree

9 files changed

+116
-198
lines changed

9 files changed

+116
-198
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ A brief description of the categories of changes:
7979
### Removed
8080
* (toolchains): Removed accidentally exposed `http_archive` symbol from
8181
`python/repositories.bzl`.
82+
* (toolchains): An internal _is_python_config_setting_ macro has been removed.
8283

8384
## [0.35.0] - 2024-08-15
8485

@@ -274,7 +275,7 @@ A brief description of the categories of changes:
274275
be automatically deleted correctly. For example, if `python_generation_mode`
275276
is set to package, when `__init__.py` is deleted, the `py_library` generated
276277
for this package before will be deleted automatically.
277-
* (whl_library): Use `is_python_config_setting` to correctly handle multi-python
278+
* (whl_library): Use _is_python_config_setting_ to correctly handle multi-python
278279
version dependency select statements when the `experimental_target_platforms`
279280
includes the Python ABI. The default python version case within the select is
280281
also now handled correctly, stabilizing the implementation.

examples/bzlmod/MODULE.bazel.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/config_settings/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
2+
load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
23
load(
34
"//python/private:flags.bzl",
45
"BootstrapImplFlag",
@@ -27,6 +28,8 @@ filegroup(
2728

2829
construct_config_settings(
2930
name = "construct_config_settings",
31+
minor_mapping = MINOR_MAPPING,
32+
versions = TOOL_VERSIONS.keys(),
3033
)
3134

3235
string_flag(

python/config_settings/config_settings.bzl

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@
1818
load(
1919
"//python/private:config_settings.bzl",
2020
_construct_config_settings = "construct_config_settings",
21-
_is_python_config_setting = "is_python_config_setting",
2221
)
2322

24-
# This is exposed only for cases where the pip hub repo needs to use this rule
25-
# to define hub-repo scoped config_settings for platform specific wheel
26-
# support.
27-
is_python_config_setting = _is_python_config_setting
28-
2923
# This is exposed for usage in rules_python only.
3024
construct_config_settings = _construct_config_settings

python/private/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ bzl_library(
7878
name = "config_settings_bzl",
7979
srcs = ["config_settings.bzl"],
8080
deps = [
81-
"//python:versions_bzl",
81+
":semver_bzl",
8282
"@bazel_skylib//lib:selects",
83+
"@bazel_skylib//rules:common_settings",
8384
],
8485
)
8586

python/private/config_settings.bzl

Lines changed: 79 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -17,201 +17,95 @@
1717

1818
load("@bazel_skylib//lib:selects.bzl", "selects")
1919
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
20-
load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
20+
load(":semver.bzl", "semver")
2121

22-
_PYTHON_VERSION_FLAG = str(Label("//python/config_settings:python_version"))
22+
_PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version")
23+
_PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:_python_version_major_minor")
2324

24-
def _ver_key(s):
25-
major, _, s = s.partition(".")
26-
minor, _, s = s.partition(".")
27-
micro, _, s = s.partition(".")
28-
return (int(major), int(minor), int(micro))
29-
30-
def _flag_values(*, python_versions, minor_mapping):
31-
"""Construct a map of python_version to a list of toolchain values.
32-
33-
This mapping maps the concept of a config setting to a list of compatible toolchain versions.
34-
For using this in the code, the VERSION_FLAG_VALUES should be used instead.
35-
36-
Args:
37-
python_versions: {type}`list[str]` X.Y.Z` python versions.
38-
minor_mapping: {type}`dict[str, str]` `X.Y` to `X.Y.Z` mapping.
39-
40-
Returns:
41-
A `map[str, list[str]]`. Each key is a python_version flag value. Each value
42-
is a list of the python_version flag values that should match when for the
43-
`key`. For example:
44-
```
45-
"3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions
46-
"3.8.2" -> ["3.8.2"] # Only 3.8.2
47-
"3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so
48-
as when the `3.8` toolchain is used we just use the latest `3.8` toolchain.
49-
this makes the `select("is_python_3.8.19")` work no matter how the user
50-
specifies the latest python version to use.
51-
```
52-
"""
53-
ret = {}
54-
55-
for micro_version in sorted(python_versions, key = _ver_key):
56-
minor_version, _, _ = micro_version.rpartition(".")
57-
58-
# This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
59-
# It's private because matching the concept of e.g. "3.8" value is done
60-
# using the `is_python_X.Y` config setting group, which is aware of the
61-
# minor versions that could match instead.
62-
ret.setdefault(minor_version, [minor_version]).append(micro_version)
63-
64-
# Ensure that is_python_3.9.8 is matched if python_version is set
65-
# to 3.9 if minor_mapping points to 3.9.8
66-
default_micro_version = minor_mapping[minor_version]
67-
ret[micro_version] = [micro_version, minor_version] if default_micro_version == micro_version else [micro_version]
68-
69-
return ret
70-
71-
VERSION_FLAG_VALUES = _flag_values(python_versions = TOOL_VERSIONS.keys(), minor_mapping = MINOR_MAPPING)
72-
73-
def is_python_config_setting(name, *, python_version, reuse_conditions = None, **kwargs):
74-
"""Create a config setting for matching 'python_version' configuration flag.
75-
76-
This function is mainly intended for internal use within the `whl_library` and `pip_parse`
77-
machinery.
78-
79-
The matching of the 'python_version' flag depends on the value passed in
80-
`python_version` and here is the example for `3.8` (but the same applies
81-
to other python versions present in @//python:versions.bzl#TOOL_VERSIONS):
82-
* "3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions
83-
* "3.8.2" -> ["3.8.2"] # Only 3.8.2
84-
* "3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so
85-
as when the `3.8` toolchain is used we just use the latest `3.8` toolchain.
86-
this makes the `select("is_python_3.8.19")` work no matter how the user
87-
specifies the latest python version to use.
88-
89-
Args:
90-
name: name for the target that will be created to be used in select statements.
91-
python_version: The python_version to be passed in the `flag_values` in the
92-
`config_setting`. Depending on the version, the matching python version list
93-
can be as described above.
94-
reuse_conditions: A dict of version to version label for which we should
95-
reuse config_setting targets instead of creating them from scratch. This
96-
is useful when using is_python_config_setting multiple times in the
97-
same package with the same `major.minor` python versions.
98-
**kwargs: extra kwargs passed to the `config_setting`.
99-
"""
100-
if python_version not in name:
101-
fail("The name '{}' must have the python version '{}' in it".format(name, python_version))
102-
103-
if python_version not in VERSION_FLAG_VALUES:
104-
fail("The 'python_version' must be known to 'rules_python', choose from the values: {}".format(VERSION_FLAG_VALUES.keys()))
105-
106-
python_versions = VERSION_FLAG_VALUES[python_version]
107-
extra_flag_values = kwargs.pop("flag_values", {})
108-
if _PYTHON_VERSION_FLAG in extra_flag_values:
109-
fail("Cannot set '{}' in the flag values".format(_PYTHON_VERSION_FLAG))
110-
111-
if len(python_versions) == 1:
112-
native.config_setting(
113-
name = name,
114-
flag_values = {
115-
_PYTHON_VERSION_FLAG: python_version,
116-
} | extra_flag_values,
117-
**kwargs
118-
)
119-
return
120-
121-
reuse_conditions = reuse_conditions or {}
122-
create_config_settings = {
123-
"_{}".format(name).replace(python_version, version): {_PYTHON_VERSION_FLAG: version}
124-
for version in python_versions
125-
if not reuse_conditions or version not in reuse_conditions
126-
}
127-
match_any = list(create_config_settings.keys())
128-
for version, condition in reuse_conditions.items():
129-
if len(VERSION_FLAG_VALUES[version]) == 1:
130-
match_any.append(condition)
131-
continue
132-
133-
# Convert the name to an internal label that this function would create,
134-
# so that we are hitting the config_setting and not the config_setting_group.
135-
condition = Label(condition)
136-
if hasattr(condition, "same_package_label"):
137-
condition = condition.same_package_label("_" + condition.name)
138-
else:
139-
condition = condition.relative("_" + condition.name)
140-
141-
match_any.append(condition)
142-
143-
for name_, flag_values_ in create_config_settings.items():
144-
native.config_setting(
145-
name = name_,
146-
flag_values = flag_values_ | extra_flag_values,
147-
**kwargs
148-
)
149-
150-
# An alias pointing to an underscore-prefixed config_setting_group
151-
# is used because config_setting_group creates
152-
# `is_{version}_N` targets, which are easily confused with the
153-
# `is_{minor}.{micro}` (dot) targets.
154-
selects.config_setting_group(
155-
name = "_{}_group".format(name),
156-
match_any = match_any,
157-
visibility = ["//visibility:private"],
158-
)
159-
native.alias(
160-
name = name,
161-
actual = "_{}_group".format(name),
162-
visibility = kwargs.get("visibility", []),
163-
)
164-
165-
def construct_config_settings(name = None): # buildifier: disable=function-docstring
25+
def construct_config_settings(*, name, versions, minor_mapping): # buildifier: disable=function-docstring
16626
"""Create a 'python_version' config flag and construct all config settings used in rules_python.
16727
16828
This mainly includes the targets that are used in the toolchain and pip hub
16929
repositories that only match on the 'python_version' flag values.
17030
17131
Args:
172-
name(str): A dummy name value that is no-op for now.
32+
name: {type}`str` A dummy name value that is no-op for now.
33+
versions: {type}`list[str]` A list of versions to build constraint settings for.
34+
minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions.
17335
"""
36+
_ = name # @unused
17437
_python_version_flag(
175-
name = "python_version",
38+
name = _PYTHON_VERSION_FLAG.name,
17639
# TODO: The default here should somehow match the MODULE config. Until
17740
# then, use the empty string to indicate an unknown version. This
17841
# also prevents version-unaware targets from inadvertently matching
17942
# a select condition when they shouldn't.
18043
build_setting_default = "",
181-
values = [""] + VERSION_FLAG_VALUES.keys(),
44+
visibility = ["//visibility:public"],
45+
)
46+
47+
_python_version_major_minor_flag(
48+
name = _PYTHON_VERSION_MAJOR_MINOR_FLAG.name,
49+
build_setting_default = "",
18250
visibility = ["//visibility:public"],
18351
)
18452

18553
native.config_setting(
18654
name = "is_python_version_unset",
187-
flag_values = {
188-
Label("//python/config_settings:python_version"): "",
189-
},
55+
flag_values = {_PYTHON_VERSION_FLAG: ""},
19056
visibility = ["//visibility:public"],
19157
)
19258

193-
for version, matching_versions in VERSION_FLAG_VALUES.items():
194-
is_python_config_setting(
195-
name = "is_python_{}".format(version),
196-
python_version = version,
197-
reuse_conditions = {
198-
v: native.package_relative_label("is_python_{}".format(v))
199-
for v in matching_versions
200-
if v != version
201-
},
59+
_reverse_minor_mapping = {full: minor for minor, full in minor_mapping.items()}
60+
for version in versions:
61+
minor_version = _reverse_minor_mapping.get(version)
62+
if not minor_version:
63+
native.config_setting(
64+
name = "is_python_{}".format(version),
65+
flag_values = {":python_version": version},
66+
visibility = ["//visibility:public"],
67+
)
68+
continue
69+
70+
# Also need to match the minor version when using
71+
name = "is_python_{}".format(version)
72+
native.config_setting(
73+
name = "_" + name,
74+
flag_values = {":python_version": version},
75+
visibility = ["//visibility:public"],
76+
)
77+
78+
# An alias pointing to an underscore-prefixed config_setting_group
79+
# is used because config_setting_group creates
80+
# `is_{version}_N` targets, which are easily confused with the
81+
# `is_{minor}.{micro}` (dot) targets.
82+
selects.config_setting_group(
83+
name = "_{}_group".format(name),
84+
match_any = [
85+
":_is_python_{}".format(version),
86+
":is_python_{}".format(minor_version),
87+
],
88+
visibility = ["//visibility:private"],
89+
)
90+
native.alias(
91+
name = name,
92+
actual = "_{}_group".format(name),
93+
visibility = ["//visibility:public"],
94+
)
95+
96+
# This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
97+
# It's private because matching the concept of e.g. "3.8" value is done
98+
# using the `is_python_X.Y` config setting group, which is aware of the
99+
# minor versions that could match instead.
100+
for minor in minor_mapping.keys():
101+
native.config_setting(
102+
name = "is_python_{}".format(minor),
103+
flag_values = {_PYTHON_VERSION_MAJOR_MINOR_FLAG: minor},
202104
visibility = ["//visibility:public"],
203105
)
204106

205107
def _python_version_flag_impl(ctx):
206108
value = ctx.build_setting_value
207-
if value not in ctx.attr.values:
208-
fail((
209-
"Invalid --python_version value: {actual}\nAllowed values {allowed}"
210-
).format(
211-
actual = value,
212-
allowed = ", ".join(sorted(ctx.attr.values)),
213-
))
214-
215109
return [
216110
# BuildSettingInfo is the original provider returned, so continue to
217111
# return it for compatibility
@@ -227,9 +121,25 @@ def _python_version_flag_impl(ctx):
227121
_python_version_flag = rule(
228122
implementation = _python_version_flag_impl,
229123
build_setting = config.string(flag = True),
124+
attrs = {},
125+
)
126+
127+
def _python_version_major_minor_flag_impl(ctx):
128+
input = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value
129+
if input:
130+
version = semver(input)
131+
value = "{}.{}".format(version.major, version.minor)
132+
else:
133+
value = ""
134+
135+
return [config_common.FeatureFlagInfo(value = value)]
136+
137+
_python_version_major_minor_flag = rule(
138+
implementation = _python_version_major_minor_flag_impl,
139+
build_setting = config.string(flag = False),
230140
attrs = {
231-
"values": attr.string_list(
232-
doc = "Allowed values.",
141+
"_python_version_flag": attr.label(
142+
default = _PYTHON_VERSION_FLAG,
233143
),
234144
},
235145
)

python/private/pypi/generate_whl_library_build_bazel.bzl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,13 @@ def _render_config_settings(dependencies_by_platform):
157157
constraint_values_str = render.indent(render.list(constraint_values)).lstrip()
158158

159159
if abi:
160-
if not loads:
161-
loads.append("""load("@rules_python//python/config_settings:config_settings.bzl", "is_python_config_setting")""")
162-
163160
additional_content.append(
164161
"""\
165-
is_python_config_setting(
162+
config_setting(
166163
name = "is_{name}",
167-
python_version = "3.{minor_version}",
164+
flag_values = {{
165+
"@rules_python//python/config_settings:_python_version_major_minor": "3.{minor_version}",
166+
}},
168167
constraint_values = {constraint_values},
169168
visibility = ["//visibility:private"],
170169
)""".format(

0 commit comments

Comments
 (0)