@@ -78,6 +78,47 @@ 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 , watch = "yes" ).strip (),
115+ )
116+ if default_python_version_env :
117+ default_python_version = module_ctx .getenv (
118+ default_python_version_env ,
119+ default_python_version ,
120+ )
121+
81122 seen_versions = {}
82123 for mod in module_ctx .modules :
83124 module_toolchain_versions = []
@@ -104,7 +145,13 @@ def parse_modules(*, module_ctx, _fail = fail):
104145 # * rules_python needs to set a soft default in case the root module doesn't,
105146 # e.g. if the root module doesn't use Python itself.
106147 # * The root module is allowed to override the rules_python default.
107- is_default = toolchain_attr .is_default
148+ if default_python_version :
149+ is_default = default_python_version == toolchain_version
150+ if toolchain_attr .is_default and not is_default :
151+ fail ("The 'is_default' attribute doesn't work if you set " +
152+ "the default Python version with the `defaults` tag." )
153+ else :
154+ is_default = toolchain_attr .is_default
108155
109156 # Also only the root module should be able to decide ignore_root_user_error.
110157 # Modules being depended upon don't know the final environment, so they aren't
@@ -115,7 +162,7 @@ def parse_modules(*, module_ctx, _fail = fail):
115162 fail ("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes" )
116163
117164 ignore_root_user_error = toolchain_attr .ignore_root_user_error
118- elif mod .name == "rules_python" and not default_toolchain :
165+ elif mod .name == "rules_python" and not default_toolchain and not default_python_version :
119166 # We don't do the len() check because we want the default that rules_python
120167 # sets to be clearly visible.
121168 is_default = toolchain_attr .is_default
@@ -282,6 +329,19 @@ def _python_impl(module_ctx):
282329 else :
283330 return None
284331
332+ def _one_or_the_same (first , second , * , onerror = None ):
333+ if not first :
334+ return second
335+ if not second or second == first :
336+ return first
337+ if onerror :
338+ return onerror (first , second )
339+ else :
340+ fail ("Unique value needed, got both '{}' and '{}', which are different" .format (
341+ first ,
342+ second ,
343+ ))
344+
285345def _fail_duplicate_module_toolchain_version (version , module ):
286346 fail (("Duplicate module toolchain version: module '{module}' attempted " +
287347 "to use version '{version}' multiple times in itself" ).format (
@@ -305,6 +365,30 @@ def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_na
305365 version = version ,
306366 ))
307367
368+ def _fail_multiple_defaults_python_version (first , second ):
369+ fail (("Multiple python_version entries in defaults: " +
370+ "First default was python_version '{first}'. " +
371+ "Second was python_version '{second}'" ).format (
372+ first = first ,
373+ second = second ,
374+ ))
375+
376+ def _fail_multiple_defaults_python_version_file (first , second ):
377+ fail (("Multiple python_version_file entries in defaults: " +
378+ "First default was python_version_file '{first}'. " +
379+ "Second was python_version_file '{second}'" ).format (
380+ first = first ,
381+ second = second ,
382+ ))
383+
384+ def _fail_multiple_defaults_python_version_env (first , second ):
385+ fail (("Multiple python_version_env entries in defaults: " +
386+ "First default was python_version_env '{first}'. " +
387+ "Second was python_version_env '{second}'" ).format (
388+ first = first ,
389+ second = second ,
390+ ))
391+
308392def _fail_multiple_default_toolchains (first , second ):
309393 fail (("Multiple default toolchains: only one toolchain " +
310394 "can have is_default=True. First default " +
@@ -526,6 +610,21 @@ def _get_toolchain_config(*, modules, _fail = fail):
526610 register_all_versions = register_all_versions ,
527611 )
528612
613+ def _create_defaults_attr_structs (* , mod ):
614+ arg_structs = []
615+
616+ for tag in mod .tags .defaults :
617+ arg_structs .append (_create_defaults_attr_struct (tag = tag ))
618+
619+ return arg_structs
620+
621+ def _create_defaults_attr_struct (* , tag ):
622+ return struct (
623+ python_version = getattr (tag , "python_version" , None ),
624+ python_version_env = getattr (tag , "python_version_env" , None ),
625+ python_version_file = getattr (tag , "python_version_file" , None ),
626+ )
627+
529628def _create_toolchain_attr_structs (* , mod , config , seen_versions ):
530629 arg_structs = []
531630
@@ -570,6 +669,49 @@ def _get_bazel_version_specific_kwargs():
570669
571670 return kwargs
572671
672+ _defaults = tag_class (
673+ doc = """Tag class to specify the default Python version.""" ,
674+ attrs = {
675+ "python_version" : attr .string (
676+ mandatory = False ,
677+ doc = """\
678+ String saying what the default Python version should be. If the string
679+ matches the {attr}`python_version` attribute of a toolchain, this
680+ toolchain is the default version. If this attribute is set, the
681+ {attr}`is_default` attribute of the toolchain is ignored.
682+
683+ :::{versionadded} VERSION_NEXT_FEATURE
684+ :::
685+ """ ,
686+ ),
687+ "python_version_env" : attr .string (
688+ mandatory = False ,
689+ doc = """\
690+ Environment variable saying what the default Python version should be.
691+ If the string matches the {attr}`python_version` attribute of a
692+ toolchain, this toolchain is the default version. If this attribute is
693+ set, the {attr}`is_default` attribute of the toolchain is ignored.
694+
695+ :::{versionadded} VERSION_NEXT_FEATURE
696+ :::
697+ """ ,
698+ ),
699+ "python_version_file" : attr .label (
700+ mandatory = False ,
701+ allow_single_file = True ,
702+ doc = """\
703+ File saying what the default Python version should be. If the contents
704+ of the file match the {attr}`python_version` attribute of a toolchain,
705+ this toolchain is the default version. If this attribute is set, the
706+ {attr}`is_default` attribute of the toolchain is ignored.
707+
708+ :::{versionadded} VERSION_NEXT_FEATURE
709+ :::
710+ """ ,
711+ ),
712+ },
713+ )
714+
573715_toolchain = tag_class (
574716 doc = """Tag class used to register Python toolchains.
575717Use this tag class to register one or more Python toolchains. This class
@@ -653,7 +795,14 @@ error to run with root access instead.
653795 ),
654796 "is_default" : attr .bool (
655797 mandatory = False ,
656- doc = "Whether the toolchain is the default version" ,
798+ doc = """\
799+ Whether the toolchain is the default version.
800+
801+ :::{versionchanged} VERSION_NEXT_FEATURE
802+ This setting is ignored if the default version is set using the `defaults`
803+ tag class.
804+ :::
805+ """ ,
657806 ),
658807 "python_version" : attr .string (
659808 mandatory = True ,
@@ -852,6 +1001,7 @@ python = module_extension(
8521001""" ,
8531002 implementation = _python_impl ,
8541003 tag_classes = {
1004+ "defaults" : _defaults ,
8551005 "override" : _override ,
8561006 "single_version_override" : _single_version_override ,
8571007 "single_version_platform_override" : _single_version_platform_override ,
0 commit comments