@@ -78,6 +78,47 @@ def parse_modules(*, module_ctx, _fail = fail):
78
78
79
79
config = _get_toolchain_config (modules = module_ctx .modules , _fail = _fail )
80
80
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
+
81
122
seen_versions = {}
82
123
for mod in module_ctx .modules :
83
124
module_toolchain_versions = []
@@ -104,7 +145,13 @@ def parse_modules(*, module_ctx, _fail = fail):
104
145
# * rules_python needs to set a soft default in case the root module doesn't,
105
146
# e.g. if the root module doesn't use Python itself.
106
147
# * 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
108
155
109
156
# Also only the root module should be able to decide ignore_root_user_error.
110
157
# 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):
115
162
fail ("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes" )
116
163
117
164
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 :
119
166
# We don't do the len() check because we want the default that rules_python
120
167
# sets to be clearly visible.
121
168
is_default = toolchain_attr .is_default
@@ -282,6 +329,19 @@ def _python_impl(module_ctx):
282
329
else :
283
330
return None
284
331
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
+
285
345
def _fail_duplicate_module_toolchain_version (version , module ):
286
346
fail (("Duplicate module toolchain version: module '{module}' attempted " +
287
347
"to use version '{version}' multiple times in itself" ).format (
@@ -305,6 +365,30 @@ def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_na
305
365
version = version ,
306
366
))
307
367
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
+
308
392
def _fail_multiple_default_toolchains (first , second ):
309
393
fail (("Multiple default toolchains: only one toolchain " +
310
394
"can have is_default=True. First default " +
@@ -526,6 +610,21 @@ def _get_toolchain_config(*, modules, _fail = fail):
526
610
register_all_versions = register_all_versions ,
527
611
)
528
612
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
+
529
628
def _create_toolchain_attr_structs (* , mod , config , seen_versions ):
530
629
arg_structs = []
531
630
@@ -570,6 +669,49 @@ def _get_bazel_version_specific_kwargs():
570
669
571
670
return kwargs
572
671
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
+
573
715
_toolchain = tag_class (
574
716
doc = """Tag class used to register Python toolchains.
575
717
Use this tag class to register one or more Python toolchains. This class
@@ -653,7 +795,14 @@ error to run with root access instead.
653
795
),
654
796
"is_default" : attr .bool (
655
797
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
+ """ ,
657
806
),
658
807
"python_version" : attr .string (
659
808
mandatory = True ,
@@ -852,6 +1001,7 @@ python = module_extension(
852
1001
""" ,
853
1002
implementation = _python_impl ,
854
1003
tag_classes = {
1004
+ "defaults" : _defaults ,
855
1005
"override" : _override ,
856
1006
"single_version_override" : _single_version_override ,
857
1007
"single_version_platform_override" : _single_version_platform_override ,
0 commit comments