From 68130551a60c5b3bac5c5b61c327256402da5c13 Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Tue, 28 Jan 2025 15:27:34 +0100 Subject: [PATCH 01/10] feat(python.toolchain): support file-based default Python version This change adds a new `default_version_file` attribute to `python.toolchain`. If set, the toolchain compares the file's contents to its `python_version`, and if they match, treats that toolchain as default (ignoring `is_default`). This allows Bazel to synchronize the default Python version with external tools (e.g., pyenv) that use a `.python-version` file or environment variables. Fixes #2587. --- python/private/python.bzl | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index ec6f73e41f..ef99be8d0f 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -104,7 +104,14 @@ def parse_modules(*, module_ctx, _fail = fail): # * rules_python needs to set a soft default in case the root module doesn't, # e.g. if the root module doesn't use Python itself. # * The root module is allowed to override the rules_python default. - is_default = toolchain_attr.is_default + if toolchain_attr.default_version_file == None: + is_default = toolchain_attr.is_default + else: + is_default = ( + module_ctx.read(toolchain_attr.default_version_file) == toolchain_version + ) + if toolchain_attr.is_default and not is_default: + fail("The 'is_default' attribute doesn't work if you set 'default_version_file'.") # Also only the root module should be able to decide ignore_root_user_error. # Modules being depended upon don't know the final environment, so they aren't @@ -560,6 +567,7 @@ def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolcha python_version = python_version if python_version else tag.python_version, configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), ignore_root_user_error = getattr(tag, "ignore_root_user_error", False), + default_version_file = getattr(tag, "default_version_file", None), ) def _get_bazel_version_specific_kwargs(): @@ -635,6 +643,18 @@ Then the python interpreter will be available as `my_python_name`. mandatory = False, doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains.", ), + "default_version_file": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +File saying what the default Python version should be. If the contents of the +file match the `python_version` attribute, this toolchain is the default version. +If this attribute is set, the `is_default` attribute is ignored. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), "ignore_root_user_error": attr.bool( default = False, doc = """\ @@ -651,7 +671,9 @@ can result in spurious build failures. ), "is_default": attr.bool( mandatory = False, - doc = "Whether the toolchain is the default version", + doc = """\ +Whether the toolchain is the default version. Ignored if `default_version_file` +is set.""", ), "python_version": attr.string( mandatory = True, From e0b7a3d1b192d4a583935d4c5a649927e3947a53 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:57:54 +0900 Subject: [PATCH 02/10] Update python/private/python.bzl --- python/private/python.bzl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index ef99be8d0f..16ecf157ac 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -672,8 +672,12 @@ can result in spurious build failures. "is_default": attr.bool( mandatory = False, doc = """\ -Whether the toolchain is the default version. Ignored if `default_version_file` -is set.""", +Whether the toolchain is the default version. + +:::{versionchanged} VERSION_NEXT_FEATURE +This setting is ignored if {attr}`default_version_file` is set. +::: +""", ), "python_version": attr.string( mandatory = True, From f2cc08ad6c362f54ed6f01b42eb2e8d1d547b650 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:58:01 +0900 Subject: [PATCH 03/10] Update python/private/python.bzl --- python/private/python.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 16ecf157ac..2066988fd2 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -648,8 +648,8 @@ Then the python interpreter will be available as `my_python_name`. allow_single_file = True, doc = """\ File saying what the default Python version should be. If the contents of the -file match the `python_version` attribute, this toolchain is the default version. -If this attribute is set, the `is_default` attribute is ignored. +file match the {attr}`python_version` attribute, this toolchain is the default version. +If this attribute is set, the {attr}`is_default` attribute is ignored. :::{versionadded} VERSION_NEXT_FEATURE ::: From 31cb09c25ea0bc145fbdc9f58d20acc28d09217b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:58:32 +0900 Subject: [PATCH 04/10] Update python/private/python.bzl --- python/private/python.bzl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 2066988fd2..17982452d0 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -104,12 +104,11 @@ def parse_modules(*, module_ctx, _fail = fail): # * rules_python needs to set a soft default in case the root module doesn't, # e.g. if the root module doesn't use Python itself. # * The root module is allowed to override the rules_python default. - if toolchain_attr.default_version_file == None: - is_default = toolchain_attr.is_default + if toolchain_attr.default_version_file: + version_from_file = module_ctx.read(toolchain_attr.default_version_file).strip() + is_default = version_from_file == toolchain_version else: - is_default = ( - module_ctx.read(toolchain_attr.default_version_file) == toolchain_version - ) + is_default = toolchain_attr.is_default if toolchain_attr.is_default and not is_default: fail("The 'is_default' attribute doesn't work if you set 'default_version_file'.") From 86d17aa53f63a890e212c1c045688586c4f91c09 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 06:03:59 +0000 Subject: [PATCH 05/10] doc: add changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8da580a1..dddf2522b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,9 @@ Unreleased changes template. {#v0-0-0-added} ### Added -* Nothing added. +* (python) {attr}`python.toolchain.default_version_file` has been added to + allow users to set the default python version in the root module by reading + the default version number from a file. {#v0-0-0-removed} ### Removed From 2b1286df5e17f5d824c85c032b655ef6b41a20a9 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 06:05:41 +0000 Subject: [PATCH 06/10] adjust changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af02bb5349..79978618df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ Unreleased changes template. {#v0-0-0-added} ### Added +* (python) {attr}`python.toolchain.default_version_file` has been added to + allow users to set the default python version in the root module by reading + the default version number from a file. * {obj}`//python/bin:python`: convenience target for directly running an interpreter. {obj}`--//python/bin:python_src` can be used to specify a binary whose interpreter to use. @@ -124,9 +127,7 @@ Unreleased changes template. {#v1-2-0-added} ### Added -* (python) {attr}`python.toolchain.default_version_file` has been added to - allow users to set the default python version in the root module by reading - the default version number from a file. +* Nothing added. {#v1-2-0-removed} ### Removed From 4acb4d7b8334f74d3392da3fa3a4feaf9bf5d9b2 Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Tue, 18 Mar 2025 17:41:39 +0100 Subject: [PATCH 07/10] feat: Implement a python.defaults tag class As an alternative to python.toolchain.is_default, introduce a python.defaults tag class with attributes python_version, python_version_env and python_version_file. This allows to read the default python version from your projects .python-version files, similar to other tools. It also allows using an environment variable, with a fallback if the environment variable is not set. --- CHANGELOG.md | 6 +- examples/multi_python_versions/MODULE.bazel | 7 +- python/private/python.bzl | 176 +++++++++++++++++--- tests/python/python_tests.bzl | 121 +++++++++++++- 4 files changed, 282 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97c1c344b..c1030eeb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,9 +78,9 @@ Unreleased changes template. {#v0-0-0-added} ### Added -* (python) {attr}`python.toolchain.default_version_file` has been added to - allow users to set the default python version in the root module by reading - the default version number from a file. +* (python) {attr}`python.defaults` has been added to allow users to + set the default python version in the root module by reading the + default version number from a file or an environment variable. * {obj}`//python/bin:python`: convenience target for directly running an interpreter. {obj}`--//python/bin:python_src` can be used to specify a binary whose interpreter to use. diff --git a/examples/multi_python_versions/MODULE.bazel b/examples/multi_python_versions/MODULE.bazel index 578315741f..4e112d5b13 100644 --- a/examples/multi_python_versions/MODULE.bazel +++ b/examples/multi_python_versions/MODULE.bazel @@ -10,14 +10,17 @@ local_path_override( ) python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.defaults( + # The environment variable takes precedence if set. + python_version = "3.9", + python_version_env = "BAZEL_PYTHON_VERSION", +) python.toolchain( configure_coverage_tool = True, python_version = "3.8", ) python.toolchain( configure_coverage_tool = True, - # Only set when you have mulitple toolchain versions. - is_default = True, python_version = "3.9", ) python.toolchain( diff --git a/python/private/python.bzl b/python/private/python.bzl index d385a4f5ae..717591c904 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -78,6 +78,55 @@ def parse_modules(*, module_ctx, _fail = fail): config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) + default_python_version = None + for mod in module_ctx.modules: + defaults_attr_structs = _create_defaults_attr_structs(mod = mod) + default_python_version_env = None + default_python_version_file = None + + # Only the root module and rules_python are allowed to specify the default + # toolchain for a couple reasons: + # * It prevents submodules from specifying different defaults and only + # one of them winning. + # * rules_python needs to set a soft default in case the root module doesn't, + # e.g. if the root module doesn't use Python itself. + # * The root module is allowed to override the rules_python default. + if mod.is_root or (mod.name == "rules_python" and not default_python_version): + for defaults_attr in defaults_attr_structs: + default_python_version = _one_or_the_same( + default_python_version, + defaults_attr.python_version, + onerror = _fail_multiple_defaults_python_version, + ) + default_python_version_env = _one_or_the_same( + default_python_version_env, + defaults_attr.python_version_env, + onerror = _fail_multiple_defaults_python_version_env, + ) + default_python_version_file = _one_or_the_same( + default_python_version_file, + defaults_attr.python_version_file, + onerror = _fail_multiple_defaults_python_version_file, + ) + if default_python_version_file: + default_python_version = _one_or_the_same( + default_python_version, + module_ctx.read(default_python_version_file).strip(), + ) + if default_python_version_env: + # Bazel version 7.1.0 and later support module_ctx.getenv(name, default): + # When building incrementally, any change to the value of the variable + # named by `name` will cause this repository to be re-fetched. + if "getenv" in dir(module_ctx): + getenv = module_ctx.getenv + else: + getenv = module_ctx.os.environ.get + default_python_version = getenv(default_python_version_env, default_python_version) + if not default_python_version: + fallback_python_version_file = module_ctx.path("@@//:.python-version") + if fallback_python_version_file.exists: + default_python_version = module_ctx.read(fallback_python_version_file).strip() + seen_versions = {} for mod in module_ctx.modules: module_toolchain_versions = [] @@ -104,13 +153,14 @@ def parse_modules(*, module_ctx, _fail = fail): # * rules_python needs to set a soft default in case the root module doesn't, # e.g. if the root module doesn't use Python itself. # * The root module is allowed to override the rules_python default. - if toolchain_attr.default_version_file: - version_from_file = module_ctx.read(toolchain_attr.default_version_file).strip() - is_default = version_from_file == toolchain_version + if default_python_version: + is_default = default_python_version == toolchain_version + if toolchain_attr.is_default and not is_default: + fail("The 'is_default' attribute doesn't work if you set " + + "the default Python version with the `defaults` tag " + + "or the '.python-version' file.") else: is_default = toolchain_attr.is_default - if toolchain_attr.is_default and not is_default: - fail("The 'is_default' attribute doesn't work if you set 'default_version_file'.") # Also only the root module should be able to decide ignore_root_user_error. # 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): fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes") ignore_root_user_error = toolchain_attr.ignore_root_user_error - elif mod.name == "rules_python" and not default_toolchain: + elif mod.name == "rules_python" and not default_toolchain and not default_python_version: # We don't do the len() check because we want the default that rules_python # sets to be clearly visible. is_default = toolchain_attr.is_default @@ -288,6 +338,19 @@ def _python_impl(module_ctx): else: return None +def _one_or_the_same(first, second, *, onerror = None): + if not first: + return second + if not second or second == first: + return first + if onerror: + return onerror(first, second) + else: + fail("Unique value needed, got both '{}' and '{}', which are different".format( + first, + second, + )) + def _fail_duplicate_module_toolchain_version(version, module): fail(("Duplicate module toolchain version: module '{module}' attempted " + "to use version '{version}' multiple times in itself").format( @@ -311,6 +374,30 @@ def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_na version = version, )) +def _fail_multiple_defaults_python_version(first, second): + fail(("Multiple python_version entries in defaults: " + + "First default was python_version '{first}'. " + + "Second was python_version '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_defaults_python_version_file(first, second): + fail(("Multiple python_version_file entries in defaults: " + + "First default was python_version_file '{first}'. " + + "Second was python_version_file '{second}'").format( + first = first, + second = second, + )) + +def _fail_multiple_defaults_python_version_env(first, second): + fail(("Multiple python_version_env entries in defaults: " + + "First default was python_version_env '{first}'. " + + "Second was python_version_env '{second}'").format( + first = first, + second = second, + )) + def _fail_multiple_default_toolchains(first, second): fail(("Multiple default toolchains: only one toolchain " + "can have is_default=True. First default " + @@ -532,6 +619,21 @@ def _get_toolchain_config(*, modules, _fail = fail): register_all_versions = register_all_versions, ) +def _create_defaults_attr_structs(*, mod): + arg_structs = [] + + for tag in mod.tags.defaults: + arg_structs.append(_create_defaults_attr_struct(tag = tag)) + + return arg_structs + +def _create_defaults_attr_struct(*, tag): + return struct( + python_version = getattr(tag, "python_version", None), + python_version_env = getattr(tag, "python_version_env", None), + python_version_file = getattr(tag, "python_version_file", None), + ) + def _create_toolchain_attr_structs(*, mod, config, seen_versions): arg_structs = [] @@ -566,7 +668,6 @@ def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolcha python_version = python_version if python_version else tag.python_version, configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), ignore_root_user_error = getattr(tag, "ignore_root_user_error", True), - default_version_file = getattr(tag, "default_version_file", None), ) def _get_bazel_version_specific_kwargs(): @@ -577,6 +678,49 @@ def _get_bazel_version_specific_kwargs(): return kwargs +_defaults = tag_class( + doc = """Tag class to specify the default Python version.""", + attrs = { + "python_version": attr.string( + mandatory = False, + doc = """\ +String saying what the default Python version should be. If the string +matches the {attr}`python_version` attribute of a toolchain, this +toolchain is the default version. If this attribute is set, the +{attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), + "python_version_env": attr.string( + mandatory = False, + doc = """\ +Environment variable saying what the default Python version should be. +If the string matches the {attr}`python_version` attribute of a +toolchain, this toolchain is the default version. If this attribute is +set, the {attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), + "python_version_file": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +File saying what the default Python version should be. If the contents +of the file match the {attr}`python_version` attribute of a toolchain, +this toolchain is the default version. If this attribute is set, the +{attr}`is_default` attribute of the toolchain is ignored. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), + }, +) + _toolchain = tag_class( doc = """Tag class used to register Python toolchains. Use 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`. mandatory = False, doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains.", ), - "default_version_file": attr.label( - mandatory = False, - allow_single_file = True, - doc = """\ -File saying what the default Python version should be. If the contents of the -file match the {attr}`python_version` attribute, this toolchain is the default version. -If this attribute is set, the {attr}`is_default` attribute is ignored. - -:::{versionadded} VERSION_NEXT_FEATURE -::: -""", - ), "ignore_root_user_error": attr.bool( default = True, doc = """\ @@ -673,10 +805,11 @@ error to run with root access instead. "is_default": attr.bool( mandatory = False, doc = """\ -Whether the toolchain is the default version. +Whether the toolchain is the default version. :::{versionchanged} VERSION_NEXT_FEATURE -This setting is ignored if {attr}`default_version_file` is set. +This setting is ignored if the default version is set using the `defaults` +tag class. ::: """, ), @@ -877,6 +1010,7 @@ python = module_extension( """, implementation = _python_impl, tag_classes = { + "defaults": _defaults, "override": _override, "single_version_override": _single_version_override, "single_version_platform_override": _single_version_platform_override, diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 6552251331..f95476a0e4 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -20,8 +20,10 @@ load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl- _tests = [] -def _mock_mctx(*modules, environ = {}): +def _mock_mctx(*modules, environ = {}, mocked_files = {}): return struct( + path = lambda x: struct(exists = x in mocked_files, _file = x), + read = lambda x: mocked_files[x._file if "_file" in dir(x) else x], os = struct(environ = environ), modules = [ struct( @@ -39,10 +41,11 @@ def _mock_mctx(*modules, environ = {}): ], ) -def _mod(*, name, toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = True): +def _mod(*, name, defaults = [], toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = True): return struct( name = name, tags = struct( + defaults = defaults, toolchain = toolchain, override = override, single_version_override = single_version_override, @@ -51,6 +54,13 @@ def _mod(*, name, toolchain = [], override = [], single_version_override = [], s is_root = is_root, ) +def _defaults(python_version = None, python_version_env = None, python_version_file = None): + return struct( + python_version = python_version, + python_version_env = python_version_env, + python_version_file = python_version_file, + ) + def _toolchain(python_version, *, is_default = False, **kwargs): return struct( is_default = is_default, @@ -273,6 +283,113 @@ def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): _tests.append(_test_default_non_rules_python_ignore_root_user_error_non_root_module) +def _test_default_from_defaults(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_root_module", + defaults = [_defaults(python_version = "3.11")], + toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + is_root = True, + ), + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.11") + + want_toolchains = [ + struct( + name = "python_3_" + minor_version, + python_version = "3." + minor_version, + register_coverage_tool = False, + ) + for minor_version in ["10", "11", "12"] + ] + env.expect.that_collection(py.toolchains).contains_exactly(want_toolchains) + +_tests.append(_test_default_from_defaults) + +def _test_default_from_defaults_env(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_root_module", + defaults = [_defaults(python_version = "3.11", python_version_env = "PYENV_VERSION")], + toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + is_root = True, + ), + environ = {"PYENV_VERSION": "3.12"}, + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.12") + + want_toolchains = [ + struct( + name = "python_3_" + minor_version, + python_version = "3." + minor_version, + register_coverage_tool = False, + ) + for minor_version in ["10", "11", "12"] + ] + env.expect.that_collection(py.toolchains).contains_exactly(want_toolchains) + +_tests.append(_test_default_from_defaults_env) + +def _test_default_from_defaults_file(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_root_module", + defaults = [_defaults(python_version_file = "@@//:.python-version")], + toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + is_root = True, + ), + mocked_files = {"@@//:.python-version": "3.12\n"}, + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.12") + + want_toolchains = [ + struct( + name = "python_3_" + minor_version, + python_version = "3." + minor_version, + register_coverage_tool = False, + ) + for minor_version in ["10", "11", "12"] + ] + env.expect.that_collection(py.toolchains).contains_exactly(want_toolchains) + +_tests.append(_test_default_from_defaults_file) + +def _test_default_from_defaults_implicit_file(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_root_module", + defaults = [], + toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + is_root = True, + ), + mocked_files = {"@@//:.python-version": "3.12\n"}, + ), + ) + + env.expect.that_str(py.default_python_version).equals("3.12") + + want_toolchains = [ + struct( + name = "python_3_" + minor_version, + python_version = "3." + minor_version, + register_coverage_tool = False, + ) + for minor_version in ["10", "11", "12"] + ] + env.expect.that_collection(py.toolchains).contains_exactly(want_toolchains) + +_tests.append(_test_default_from_defaults_implicit_file) + def _test_first_occurance_of_the_toolchain_wins(env): py = parse_modules( module_ctx = _mock_mctx( From 678a60854671eb2083550f83b7de7039cacd1bdb Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Fri, 21 Mar 2025 12:43:08 +0100 Subject: [PATCH 08/10] Use Label when resolving @@//:.python-version Label("@@//:.python-version") resolves to the .python-version file of the root module, if such a file exists. --- python/private/python.bzl | 2 +- tests/python/python_tests.bzl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 717591c904..7ed0818049 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -123,7 +123,7 @@ def parse_modules(*, module_ctx, _fail = fail): getenv = module_ctx.os.environ.get default_python_version = getenv(default_python_version_env, default_python_version) if not default_python_version: - fallback_python_version_file = module_ctx.path("@@//:.python-version") + fallback_python_version_file = module_ctx.path(Label("@@//:.python-version")) if fallback_python_version_file.exists: default_python_version = module_ctx.read(fallback_python_version_file).strip() diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index f95476a0e4..136548e236 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -372,7 +372,7 @@ def _test_default_from_defaults_implicit_file(env): toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], is_root = True, ), - mocked_files = {"@@//:.python-version": "3.12\n"}, + mocked_files = {Label("@@//:.python-version"): "3.12\n"}, ), ) From b5a337bdc81148f7e9a4af3085fbfcd08ed52369 Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Fri, 21 Mar 2025 14:31:19 +0100 Subject: [PATCH 09/10] Drop support for Bazel 7.0 in python_version_env Implement python.defaults.python_version_env in terms of module_ctx.getenv, which was introduced in Bazel 7.1. This means that Bazel 7.0 users who wish to use the new python_version_env will have to upgrade to Bazel 7.1 or later. --- python/private/python.bzl | 12 ++++-------- tests/python/python_tests.bzl | 1 + 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index 7ed0818049..a2143b57f6 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -114,14 +114,10 @@ def parse_modules(*, module_ctx, _fail = fail): module_ctx.read(default_python_version_file).strip(), ) if default_python_version_env: - # Bazel version 7.1.0 and later support module_ctx.getenv(name, default): - # When building incrementally, any change to the value of the variable - # named by `name` will cause this repository to be re-fetched. - if "getenv" in dir(module_ctx): - getenv = module_ctx.getenv - else: - getenv = module_ctx.os.environ.get - default_python_version = getenv(default_python_version_env, default_python_version) + default_python_version = module_ctx.getenv( + default_python_version_env, + default_python_version, + ) if not default_python_version: fallback_python_version_file = module_ctx.path(Label("@@//:.python-version")) if fallback_python_version_file.exists: diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 136548e236..254ae277a6 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -24,6 +24,7 @@ def _mock_mctx(*modules, environ = {}, mocked_files = {}): return struct( path = lambda x: struct(exists = x in mocked_files, _file = x), read = lambda x: mocked_files[x._file if "_file" in dir(x) else x], + getenv = environ.get, os = struct(environ = environ), modules = [ struct( From 1a94ad46932844ca4a3cd4a393feb611214f7d82 Mon Sep 17 00:00:00 2001 From: Christian von Schultz Date: Fri, 21 Mar 2025 15:00:50 +0100 Subject: [PATCH 10/10] Explicitly watch the python version file we read If we read a python_version_file (specified with the defaults tag class or the default @@//:.python-version file), explicitly start watching that file. There are some circumstances where such a watch wouldn't be allowed, but since it's new functionality it doesn't break anything for anyone to insist on the watch. If a specific use case requires the relaxation of the requirement, that can always be considered later. --- python/private/python.bzl | 7 +++++-- tests/python/python_tests.bzl | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/python/private/python.bzl b/python/private/python.bzl index a2143b57f6..b18a700708 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -111,7 +111,7 @@ def parse_modules(*, module_ctx, _fail = fail): if default_python_version_file: default_python_version = _one_or_the_same( default_python_version, - module_ctx.read(default_python_version_file).strip(), + module_ctx.read(default_python_version_file, watch = "yes").strip(), ) if default_python_version_env: default_python_version = module_ctx.getenv( @@ -121,7 +121,10 @@ def parse_modules(*, module_ctx, _fail = fail): if not default_python_version: fallback_python_version_file = module_ctx.path(Label("@@//:.python-version")) if fallback_python_version_file.exists: - default_python_version = module_ctx.read(fallback_python_version_file).strip() + default_python_version = module_ctx.read( + fallback_python_version_file, + watch = "yes", + ).strip() seen_versions = {} for mod in module_ctx.modules: diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 254ae277a6..70d65e0ba2 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -23,7 +23,7 @@ _tests = [] def _mock_mctx(*modules, environ = {}, mocked_files = {}): return struct( path = lambda x: struct(exists = x in mocked_files, _file = x), - read = lambda x: mocked_files[x._file if "_file" in dir(x) else x], + read = lambda x, watch = None: mocked_files[x._file if "_file" in dir(x) else x], getenv = environ.get, os = struct(environ = environ), modules = [