@@ -78,6 +78,55 @@ def parse_modules(*, module_ctx, _fail = fail):
7878
7979 config = _get_toolchain_config (modules = module_ctx .modules , _fail = _fail )
8080
81+ default_python_version = None
82+ for mod in module_ctx .modules :
83+ defaults_attr_structs = _create_defaults_attr_structs (mod = mod )
84+ default_python_version_env = None
85+ default_python_version_file = None
86+
87+ # Only the root module and rules_python are allowed to specify the default
88+ # toolchain for a couple reasons:
89+ # * It prevents submodules from specifying different defaults and only
90+ # one of them winning.
91+ # * rules_python needs to set a soft default in case the root module doesn't,
92+ # e.g. if the root module doesn't use Python itself.
93+ # * The root module is allowed to override the rules_python default.
94+ if mod .is_root or (mod .name == "rules_python" and not default_python_version ):
95+ for defaults_attr in defaults_attr_structs :
96+ default_python_version = _one_or_the_same (
97+ default_python_version ,
98+ defaults_attr .python_version ,
99+ onerror = _fail_multiple_defaults_python_version ,
100+ )
101+ default_python_version_env = _one_or_the_same (
102+ default_python_version_env ,
103+ defaults_attr .python_version_env ,
104+ onerror = _fail_multiple_defaults_python_version_env ,
105+ )
106+ default_python_version_file = _one_or_the_same (
107+ default_python_version_file ,
108+ defaults_attr .python_version_file ,
109+ onerror = _fail_multiple_defaults_python_version_file ,
110+ )
111+ if default_python_version_file :
112+ default_python_version = _one_or_the_same (
113+ default_python_version ,
114+ module_ctx .read (default_python_version_file ).strip (),
115+ )
116+ if default_python_version_env :
117+ # Bazel version 7.1.0 and later support module_ctx.getenv(name, default):
118+ # When building incrementally, any change to the value of the variable
119+ # named by `name` will cause this repository to be re-fetched.
120+ if "getenv" in dir (module_ctx ):
121+ getenv = module_ctx .getenv
122+ else :
123+ getenv = module_ctx .os .environ .get
124+ default_python_version = getenv (default_python_version_env , default_python_version )
125+ if not default_python_version :
126+ fallback_python_version_file = module_ctx .path ("@@//:.python-version" )
127+ if fallback_python_version_file .exists :
128+ default_python_version = module_ctx .read (fallback_python_version_file ).strip ()
129+
81130 seen_versions = {}
82131 for mod in module_ctx .modules :
83132 module_toolchain_versions = []
@@ -104,13 +153,14 @@ def parse_modules(*, module_ctx, _fail = fail):
104153 # * rules_python needs to set a soft default in case the root module doesn't,
105154 # e.g. if the root module doesn't use Python itself.
106155 # * The root module is allowed to override the rules_python default.
107- if toolchain_attr .default_version_file :
108- version_from_file = module_ctx .read (toolchain_attr .default_version_file ).strip ()
109- is_default = version_from_file == toolchain_version
156+ if default_python_version :
157+ is_default = default_python_version == toolchain_version
158+ if toolchain_attr .is_default and not is_default :
159+ fail ("The 'is_default' attribute doesn't work if you set " +
160+ "the default Python version with the `defaults` tag " +
161+ "or the '.python-version' file." )
110162 else :
111163 is_default = toolchain_attr .is_default
112- if toolchain_attr .is_default and not is_default :
113- fail ("The 'is_default' attribute doesn't work if you set 'default_version_file'." )
114164
115165 # Also only the root module should be able to decide ignore_root_user_error.
116166 # Modules being depended upon don't know the final environment, so they aren't
@@ -121,7 +171,7 @@ def parse_modules(*, module_ctx, _fail = fail):
121171 fail ("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes" )
122172
123173 ignore_root_user_error = toolchain_attr .ignore_root_user_error
124- elif mod .name == "rules_python" and not default_toolchain :
174+ elif mod .name == "rules_python" and not default_toolchain and not default_python_version :
125175 # We don't do the len() check because we want the default that rules_python
126176 # sets to be clearly visible.
127177 is_default = toolchain_attr .is_default
@@ -288,6 +338,19 @@ def _python_impl(module_ctx):
288338 else :
289339 return None
290340
341+ def _one_or_the_same (first , second , * , onerror = None ):
342+ if not first :
343+ return second
344+ if not second or second == first :
345+ return first
346+ if onerror :
347+ return onerror (first , second )
348+ else :
349+ fail ("Unique value needed, got both '{}' and '{}', which are different" .format (
350+ first ,
351+ second ,
352+ ))
353+
291354def _fail_duplicate_module_toolchain_version (version , module ):
292355 fail (("Duplicate module toolchain version: module '{module}' attempted " +
293356 "to use version '{version}' multiple times in itself" ).format (
@@ -311,6 +374,30 @@ def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_na
311374 version = version ,
312375 ))
313376
377+ def _fail_multiple_defaults_python_version (first , second ):
378+ fail (("Multiple python_version entries in defaults: " +
379+ "First default was python_version '{first}'. " +
380+ "Second was python_version '{second}'" ).format (
381+ first = first ,
382+ second = second ,
383+ ))
384+
385+ def _fail_multiple_defaults_python_version_file (first , second ):
386+ fail (("Multiple python_version_file entries in defaults: " +
387+ "First default was python_version_file '{first}'. " +
388+ "Second was python_version_file '{second}'" ).format (
389+ first = first ,
390+ second = second ,
391+ ))
392+
393+ def _fail_multiple_defaults_python_version_env (first , second ):
394+ fail (("Multiple python_version_env entries in defaults: " +
395+ "First default was python_version_env '{first}'. " +
396+ "Second was python_version_env '{second}'" ).format (
397+ first = first ,
398+ second = second ,
399+ ))
400+
314401def _fail_multiple_default_toolchains (first , second ):
315402 fail (("Multiple default toolchains: only one toolchain " +
316403 "can have is_default=True. First default " +
@@ -532,6 +619,21 @@ def _get_toolchain_config(*, modules, _fail = fail):
532619 register_all_versions = register_all_versions ,
533620 )
534621
622+ def _create_defaults_attr_structs (* , mod ):
623+ arg_structs = []
624+
625+ for tag in mod .tags .defaults :
626+ arg_structs .append (_create_defaults_attr_struct (tag = tag ))
627+
628+ return arg_structs
629+
630+ def _create_defaults_attr_struct (* , tag ):
631+ return struct (
632+ python_version = getattr (tag , "python_version" , None ),
633+ python_version_env = getattr (tag , "python_version_env" , None ),
634+ python_version_file = getattr (tag , "python_version_file" , None ),
635+ )
636+
535637def _create_toolchain_attr_structs (* , mod , config , seen_versions ):
536638 arg_structs = []
537639
@@ -566,7 +668,6 @@ def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolcha
566668 python_version = python_version if python_version else tag .python_version ,
567669 configure_coverage_tool = getattr (tag , "configure_coverage_tool" , False ),
568670 ignore_root_user_error = getattr (tag , "ignore_root_user_error" , True ),
569- default_version_file = getattr (tag , "default_version_file" , None ),
570671 )
571672
572673def _get_bazel_version_specific_kwargs ():
@@ -577,6 +678,49 @@ def _get_bazel_version_specific_kwargs():
577678
578679 return kwargs
579680
681+ _defaults = tag_class (
682+ doc = """Tag class to specify the default Python version.""" ,
683+ attrs = {
684+ "python_version" : attr .string (
685+ mandatory = False ,
686+ doc = """\
687+ String saying what the default Python version should be. If the string
688+ matches the {attr}`python_version` attribute of a toolchain, this
689+ toolchain is the default version. If this attribute is set, the
690+ {attr}`is_default` attribute of the toolchain is ignored.
691+
692+ :::{versionadded} VERSION_NEXT_FEATURE
693+ :::
694+ """ ,
695+ ),
696+ "python_version_env" : attr .string (
697+ mandatory = False ,
698+ doc = """\
699+ Environment variable saying what the default Python version should be.
700+ If the string matches the {attr}`python_version` attribute of a
701+ toolchain, this toolchain is the default version. If this attribute is
702+ set, the {attr}`is_default` attribute of the toolchain is ignored.
703+
704+ :::{versionadded} VERSION_NEXT_FEATURE
705+ :::
706+ """ ,
707+ ),
708+ "python_version_file" : attr .label (
709+ mandatory = False ,
710+ allow_single_file = True ,
711+ doc = """\
712+ File saying what the default Python version should be. If the contents
713+ of the file match the {attr}`python_version` attribute of a toolchain,
714+ this toolchain is the default version. If this attribute is set, the
715+ {attr}`is_default` attribute of the toolchain is ignored.
716+
717+ :::{versionadded} VERSION_NEXT_FEATURE
718+ :::
719+ """ ,
720+ ),
721+ },
722+ )
723+
580724_toolchain = tag_class (
581725 doc = """Tag class used to register Python toolchains.
582726Use this tag class to register one or more Python toolchains. This class
@@ -642,18 +786,6 @@ Then the python interpreter will be available as `my_python_name`.
642786 mandatory = False ,
643787 doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains." ,
644788 ),
645- "default_version_file" : attr .label (
646- mandatory = False ,
647- allow_single_file = True ,
648- doc = """\
649- File saying what the default Python version should be. If the contents of the
650- file match the {attr}`python_version` attribute, this toolchain is the default version.
651- If this attribute is set, the {attr}`is_default` attribute is ignored.
652-
653- :::{versionadded} VERSION_NEXT_FEATURE
654- :::
655- """ ,
656- ),
657789 "ignore_root_user_error" : attr .bool (
658790 default = True ,
659791 doc = """\
@@ -673,10 +805,11 @@ error to run with root access instead.
673805 "is_default" : attr .bool (
674806 mandatory = False ,
675807 doc = """\
676- Whether the toolchain is the default version.
808+ Whether the toolchain is the default version.
677809
678810:::{versionchanged} VERSION_NEXT_FEATURE
679- This setting is ignored if {attr}`default_version_file` is set.
811+ This setting is ignored if the default version is set using the `defaults`
812+ tag class.
680813:::
681814""" ,
682815 ),
@@ -877,6 +1010,7 @@ python = module_extension(
8771010""" ,
8781011 implementation = _python_impl ,
8791012 tag_classes = {
1013+ "defaults" : _defaults ,
8801014 "override" : _override ,
8811015 "single_version_override" : _single_version_override ,
8821016 "single_version_platform_override" : _single_version_platform_override ,
0 commit comments