feat(python.toolchain): support file-based default Python version by vonschultz · Pull Request #2588 · bazel-contrib/rules_python

@vonschultz

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 bazel-contrib#2587.

aignas

aignas

@vonschultz

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.

aignas

Label("@@//:.python-version") resolves to the .python-version file of
the root module, if such a file exists.
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.
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.
The idea to read .python-version in the root module was problematic
for two reasons. For one thing, if there is a .python-version file in
the root module, but no BUILD or BUILD.bazel file on the top level, we
produce an error that we can't catch. This would be a breaking change,
requiring users to add a BUILD.bazel file. Also, if the user has a
.python-version file, and also sets is_default on a toolchain, we
would fail the build (or, if we didn't, we'd change the default Python
version against the users explicit instructions). That would also be a
breaking change.

Removing the @@//:.python-version logic means the user needs to opt in
by saying python_version_file="@@//.python-version". That's not
necessarily a bad thing, as being explicit allows anyone to grep the
repo for the label and find out what cares about that file (as pointed
out by @fmeum on Slack; good point).

@aignas

vonschultz

vonschultz added a commit to vonschultz/rules_python that referenced this pull request

May 22, 2025
When there are multiple Python toolchains, there are currently two
ways of setting the default version: the is_default attribute of
python.toolchain() tag class and the python.defaults() tag class. The
latter is more powerful, since it also supports files and environment
variables. This patch updates the examples, docs and the MODULE.bazel
file to use python.defaults().

Relates to pull request bazel-contrib#2588 and issue bazel-contrib#2587.

github-merge-queue bot pushed a commit that referenced this pull request

May 23, 2025
When there are multiple Python toolchains, there are currently two ways
of setting the default version: the `is_default` attribute of the
`python.toolchain()` tag class and the `python.defaults()` tag class.
The latter is more powerful, since it also supports files and
environment variables. This patch updates the examples and the docs to
use `python.defaults()`.

Relates to pull request #2588 and issue #2587.