Skip to content

Commit 5dfd199

Browse files
authored
chore: use python.defaults to set rules_python default python version (#3301)
python.defaults is the modern way to set the default. Setting it this way also helps avoid a bug where if a root module has a single `python.toolchain()` call (which are implicitly treated as `is_default=True`) and also sets the default using `python.defaults()`, some validation logic gives an error about using both ways to set a default.
1 parent 65f4c6e commit 5dfd199

File tree

3 files changed

+191
-94
lines changed

3 files changed

+191
-94
lines changed

MODULE.bazel

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ python = use_extension("//python/extensions:python.bzl", "python")
4242
# NOTE: This is not a stable version. It is provided for convenience, but will
4343
# change frequently to track the most recent Python version.
4444
# NOTE: The root module can override this.
45+
# NOTE: There must be a corresponding `python.toolchain()` call for the version
46+
# specified here.
47+
python.defaults(
48+
python_version = "3.11",
49+
)
4550
python.toolchain(
46-
is_default = True,
4751
python_version = "3.11",
4852
)
4953
use_repo(

python/private/python.bzl

Lines changed: 90 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -85,46 +85,7 @@ def parse_modules(*, module_ctx, logger, _fail = fail):
8585

8686
config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail)
8787

88-
default_python_version = None
89-
for mod in module_ctx.modules:
90-
defaults_attr_structs = _create_defaults_attr_structs(mod = mod)
91-
default_python_version_env = None
92-
default_python_version_file = None
93-
94-
# Only the root module and rules_python are allowed to specify the default
95-
# toolchain for a couple reasons:
96-
# * It prevents submodules from specifying different defaults and only
97-
# one of them winning.
98-
# * rules_python needs to set a soft default in case the root module doesn't,
99-
# e.g. if the root module doesn't use Python itself.
100-
# * The root module is allowed to override the rules_python default.
101-
if mod.is_root or (mod.name == "rules_python" and not default_python_version):
102-
for defaults_attr in defaults_attr_structs:
103-
default_python_version = _one_or_the_same(
104-
default_python_version,
105-
defaults_attr.python_version,
106-
onerror = _fail_multiple_defaults_python_version,
107-
)
108-
default_python_version_env = _one_or_the_same(
109-
default_python_version_env,
110-
defaults_attr.python_version_env,
111-
onerror = _fail_multiple_defaults_python_version_env,
112-
)
113-
default_python_version_file = _one_or_the_same(
114-
default_python_version_file,
115-
defaults_attr.python_version_file,
116-
onerror = _fail_multiple_defaults_python_version_file,
117-
)
118-
if default_python_version_file:
119-
default_python_version = _one_or_the_same(
120-
default_python_version,
121-
module_ctx.read(default_python_version_file, watch = "yes").strip(),
122-
)
123-
if default_python_version_env:
124-
default_python_version = module_ctx.getenv(
125-
default_python_version_env,
126-
default_python_version,
127-
)
88+
default_python_version = _compute_default_python_version(module_ctx)
12889

12990
seen_versions = {}
13091
for mod in module_ctx.modules:
@@ -152,13 +113,7 @@ def parse_modules(*, module_ctx, logger, _fail = fail):
152113
# * rules_python needs to set a soft default in case the root module doesn't,
153114
# e.g. if the root module doesn't use Python itself.
154115
# * The root module is allowed to override the rules_python default.
155-
if default_python_version:
156-
is_default = default_python_version == toolchain_version
157-
if toolchain_attr.is_default and not is_default:
158-
fail("The 'is_default' attribute doesn't work if you set " +
159-
"the default Python version with the `defaults` tag.")
160-
else:
161-
is_default = toolchain_attr.is_default
116+
is_default = default_python_version == toolchain_version
162117

163118
# Also only the root module should be able to decide ignore_root_user_error.
164119
# Modules being depended upon don't know the final environment, so they aren't
@@ -169,15 +124,15 @@ def parse_modules(*, module_ctx, logger, _fail = fail):
169124
fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes")
170125

171126
ignore_root_user_error = toolchain_attr.ignore_root_user_error
172-
elif mod.name == "rules_python" and not default_toolchain and not default_python_version:
173-
# We don't do the len() check because we want the default that rules_python
174-
# sets to be clearly visible.
175-
is_default = toolchain_attr.is_default
127+
elif mod.name == "rules_python" and not default_toolchain:
128+
# This branch handles when the root module doesn't declare a
129+
# Python toolchain
130+
is_default = default_python_version == toolchain_version
176131
else:
177132
is_default = False
178133

179134
if is_default and default_toolchain != None:
180-
_fail_multiple_default_toolchains(
135+
_fail_multiple_default_toolchains_chosen(
181136
first = default_toolchain.name,
182137
second = toolchain_name,
183138
)
@@ -577,14 +532,24 @@ def _fail_multiple_defaults_python_version_env(first, second):
577532
second = second,
578533
))
579534

580-
def _fail_multiple_default_toolchains(first, second):
535+
def _fail_multiple_default_toolchains_chosen(first, second):
581536
fail(("Multiple default toolchains: only one toolchain " +
582-
"can have is_default=True. First default " +
537+
"can be chosen as a default. First default " +
583538
"was toolchain '{first}'. Second was '{second}'").format(
584539
first = first,
585540
second = second,
586541
))
587542

543+
def _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs):
544+
fail(("Multiple default toolchains: only one toolchain " +
545+
"can have is_default=True.\n" +
546+
"Module '{module}' contains {count} toolchains with " +
547+
"is_default=True: {versions}").format(
548+
module = mod.name,
549+
count = len(toolchain_attrs),
550+
versions = ", ".join(sorted([v.python_version for v in toolchain_attrs])),
551+
))
552+
588553
def _validate_version(version_str, *, _fail = fail):
589554
v = version.parse(version_str, strict = True, _fail = _fail)
590555
if v == None:
@@ -880,6 +845,72 @@ def _get_toolchain_config(*, modules, _fail = fail):
880845
register_all_versions = register_all_versions,
881846
)
882847

848+
def _compute_default_python_version(mctx):
849+
default_python_version = None
850+
for mod in mctx.modules:
851+
# Only the root module and rules_python are allowed to specify the default
852+
# toolchain for a couple reasons:
853+
# * It prevents submodules from specifying different defaults and only
854+
# one of them winning.
855+
# * rules_python needs to set a soft default in case the root module doesn't,
856+
# e.g. if the root module doesn't use Python itself.
857+
# * The root module is allowed to override the rules_python default.
858+
if not (mod.is_root or mod.name == "rules_python"):
859+
continue
860+
861+
defaults_attr_structs = _create_defaults_attr_structs(mod = mod)
862+
default_python_version_env = None
863+
default_python_version_file = None
864+
865+
for defaults_attr in defaults_attr_structs:
866+
default_python_version = _one_or_the_same(
867+
default_python_version,
868+
defaults_attr.python_version,
869+
onerror = _fail_multiple_defaults_python_version,
870+
)
871+
default_python_version_env = _one_or_the_same(
872+
default_python_version_env,
873+
defaults_attr.python_version_env,
874+
onerror = _fail_multiple_defaults_python_version_env,
875+
)
876+
default_python_version_file = _one_or_the_same(
877+
default_python_version_file,
878+
defaults_attr.python_version_file,
879+
onerror = _fail_multiple_defaults_python_version_file,
880+
)
881+
if default_python_version_file:
882+
default_python_version = _one_or_the_same(
883+
default_python_version,
884+
mctx.read(default_python_version_file, watch = "yes").strip(),
885+
)
886+
if default_python_version_env:
887+
default_python_version = mctx.getenv(
888+
default_python_version_env,
889+
default_python_version,
890+
)
891+
892+
if default_python_version:
893+
break
894+
895+
# Otherwise, look at legacy python.toolchain() calls for a default
896+
toolchain_attrs = mod.tags.toolchain
897+
898+
# Convenience: if one python.toolchain() call exists, treat it as
899+
# the default.
900+
if len(toolchain_attrs) == 1:
901+
default_python_version = toolchain_attrs[0].python_version
902+
else:
903+
sets_default = [v for v in toolchain_attrs if v.is_default]
904+
if len(sets_default) == 1:
905+
default_python_version = sets_default[0].python_version
906+
elif len(sets_default) > 1:
907+
_fail_multiple_default_toolchains_in_module(mod, toolchain_attrs)
908+
909+
if default_python_version:
910+
break
911+
912+
return default_python_version
913+
883914
def _create_defaults_attr_structs(*, mod):
884915
arg_structs = []
885916

@@ -915,7 +946,11 @@ def _create_toolchain_attr_structs(*, mod, config, seen_versions):
915946

916947
return arg_structs
917948

918-
def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolchain_tag_count = None):
949+
def _create_toolchain_attrs_struct(
950+
*,
951+
tag = None,
952+
python_version = None,
953+
toolchain_tag_count = None):
919954
if tag and python_version:
920955
fail("Only one of tag and python version can be specified")
921956
if tag:

0 commit comments

Comments
 (0)