diff --git a/MODULE.bazel b/MODULE.bazel index 43a81619..79bdb7ac 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -13,6 +13,14 @@ bazel_dep(name = "bazel_skylib", version = "1.4.2") bazel_dep(name = "rules_python", version = "0.29.0") bazel_dep(name = "platforms", version = "0.0.7") + +# Custom python version for testing only +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = False, + python_version = "3.8.12", +) + tools = use_extension("//py:extensions.bzl", "py_tools") tools.rules_py_tools() use_repo(tools, "rules_py_tools") diff --git a/WORKSPACE b/WORKSPACE index 333c0138..d5837741 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -24,6 +24,18 @@ register_toolchains("//:container_py_toolchain") load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") +python_register_toolchains( + name = "python_toolchain_3_8", + python_version = "3.8.12", + # Setting `set_python_version_constraint` will set special constraints on the registered toolchain. + # This means that this toolchain registration will only be selected for `py_binary` / `py_test` targets + # that have the `python_version = "3.8.12"` attribute set. Targets that have no `python_attribute` will use + # the default toolchain resolved which can be seen below. + set_python_version_constraint = True, +) + +# It is important to register the default toolchain at last as it will be selected for any +# py_test/py_binary target even if it has python_version attribute set. python_register_toolchains( name = "python_toolchain", python_version = "3.9", diff --git a/docs/rules.md b/docs/rules.md index 4ad85f05..cb424ebf 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -7,7 +7,7 @@ Public API re-exports ## py_binary_rule
-py_binary_rule(name, data, deps, env, imports, main, resolutions, srcs)
+py_binary_rule(name, data, deps, env, imports, main, python_version, resolutions, srcs)
 
Run a Python program under Bazel. Most users should use the [py_binary macro](#py_binary) instead of loading this directly. @@ -23,6 +23,7 @@ Run a Python program under Bazel. Most users should use the [py_binary macro](#p | env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {} | | imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | [] | | main | Script to execute with the Python interpreter. | Label | required | | +| python_version | Whether to build this target and its transitive deps for a specific python version.

Note that setting this attribute alone will not be enough as the python toolchain for the desired version also needs to be registered in the WORKSPACE or MODULE.bazel file.

When using WORKSPACE, this may look like this,

 load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")

python_register_toolchains( name = "python_toolchain_3_8", python_version = "3.8.12", # setting set_python_version_constraint makes it so that only matches py_* rule # which has this exact version set in the python_version attribute. set_python_version_constraint = True, )

# It's important to register the default toolchain last it will match any py_* target. python_register_toolchains( name = "python_toolchain", python_version = "3.9", )


Configuring for MODULE.bazel may look like this:

 python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain(python_version = "3.8.12", is_default = False) python.toolchain(python_version = "3.9", is_default = True) 
| String | optional | "" | | resolutions | FIXME | Dictionary: Label -> String | optional | {} | | srcs | Python source files. | List of labels | optional | [] | @@ -56,7 +57,7 @@ py_library_rule(name, name, data, deps, env, imports, main, resolutions, srcs) +py_test_rule(name, data, deps, env, imports, main, python_version, resolutions, srcs) Run a Python program under Bazel. Most users should use the [py_test macro](#py_test) instead of loading this directly. @@ -72,6 +73,7 @@ Run a Python program under Bazel. Most users should use the [py_test macro](#py_ | env | Environment variables to set when running the binary. | Dictionary: String -> String | optional | {} | | imports | List of import directories to be added to the PYTHONPATH. | List of strings | optional | [] | | main | Script to execute with the Python interpreter. | Label | required | | +| python_version | Whether to build this target and its transitive deps for a specific python version.

Note that setting this attribute alone will not be enough as the python toolchain for the desired version also needs to be registered in the WORKSPACE or MODULE.bazel file.

When using WORKSPACE, this may look like this,

 load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")

python_register_toolchains( name = "python_toolchain_3_8", python_version = "3.8.12", # setting set_python_version_constraint makes it so that only matches py_* rule # which has this exact version set in the python_version attribute. set_python_version_constraint = True, )

# It's important to register the default toolchain last it will match any py_* target. python_register_toolchains( name = "python_toolchain", python_version = "3.9", )


Configuring for MODULE.bazel may look like this:

 python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain(python_version = "3.8.12", is_default = False) python.toolchain(python_version = "3.9", is_default = True) 
| String | optional | "" | | resolutions | FIXME | Dictionary: Label -> String | optional | {} | | srcs | Python source files. | List of labels | optional | [] | diff --git a/examples/multi_version/BUILD.bazel b/examples/multi_version/BUILD.bazel new file mode 100644 index 00000000..cdbf8a6b --- /dev/null +++ b/examples/multi_version/BUILD.bazel @@ -0,0 +1,38 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_pytest_main", "py_test") + +py_binary( + name = "multi_version", + srcs = ["say.py"], + deps = [ + "@pypi_cowsay//:pkg", + ], + python_version = "3.8.12" +) +py_pytest_main( + name = "__test__", + deps = ["@pypi_pytest//:pkg"], +) + +py_test( + name = "py_version_test", + srcs = [ + "py_version_test.py", + ":__test__", + ], + main = ":__test__.py", + deps = [ + ":__test__", + ], + python_version = "3.8.12" +) +py_test( + name = "py_version_default_test", + srcs = [ + "py_version_default_test.py", + ":__test__", + ], + main = ":__test__.py", + deps = [ + ":__test__", + ], +) \ No newline at end of file diff --git a/examples/multi_version/py_version_default_test.py b/examples/multi_version/py_version_default_test.py new file mode 100644 index 00000000..7b26620f --- /dev/null +++ b/examples/multi_version/py_version_default_test.py @@ -0,0 +1,5 @@ +import sys + +def test_default_py_version(): + assert sys.version_info.major == 3, "sys.version_info.major == 3" + assert sys.version_info.minor == 9, "sys.version_info.minor == 9" diff --git a/examples/multi_version/py_version_test.py b/examples/multi_version/py_version_test.py new file mode 100644 index 00000000..5fb90e65 --- /dev/null +++ b/examples/multi_version/py_version_test.py @@ -0,0 +1,6 @@ +import sys + +def test_specific_py_version(): + assert sys.version_info.major == 3, "sys.version_info.major == 3" + assert sys.version_info.minor == 8, "sys.version_info.minor == 8" + assert sys.version_info.micro == 12, "sys.version_info.micro == 12" diff --git a/examples/multi_version/say.py b/examples/multi_version/say.py new file mode 100644 index 00000000..7e0d489a --- /dev/null +++ b/examples/multi_version/say.py @@ -0,0 +1,4 @@ +import cowsay +import sys + +cowsay.cow('hello py_binary, %s!' % sys.version) \ No newline at end of file diff --git a/py/private/py_binary.bzl b/py/private/py_binary.bzl index f4edc29c..35acfd35 100644 --- a/py/private/py_binary.bzl +++ b/py/private/py_binary.bzl @@ -139,6 +139,41 @@ _attrs = dict({ allow_single_file = True, mandatory = True, ), + "python_version": attr.string( + doc = """Whether to build this target and its transitive deps for a specific python version. + +Note that setting this attribute alone will not be enough as the python toolchain for the desired version +also needs to be registered in the WORKSPACE or MODULE.bazel file. + +When using WORKSPACE, this may look like this, + +``` +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +python_register_toolchains( + name = "python_toolchain_3_8", + python_version = "3.8.12", + # setting set_python_version_constraint makes it so that only matches py_* rule + # which has this exact version set in the `python_version` attribute. + set_python_version_constraint = True, +) + +# It's important to register the default toolchain last it will match any py_* target. +python_register_toolchains( + name = "python_toolchain", + python_version = "3.9", +) +``` + +Configuring for MODULE.bazel may look like this: + +``` +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.8.12", is_default = False) +python.toolchain(python_version = "3.9", is_default = True) +``` +""" + ), "_run_tmpl": attr.label( allow_single_file = True, default = "//py/private:run.tmpl.sh", @@ -150,10 +185,25 @@ _attrs = dict({ "_interpreter_version_flag": attr.label( default = "//py:interpreter_version", ), + # Required for py_version attribute + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), }) _attrs.update(**_py_library.attrs) +def _python_version_transition_impl(_, attr): + if not attr.python_version: + return {} + return {"@rules_python//python/config_settings:python_version": str(attr.python_version)} + +_python_version_transition = transition( + implementation = _python_version_transition_impl, + inputs = [], + outputs = ["@rules_python//python/config_settings:python_version"], +) + py_base = struct( implementation = _py_binary_rule_impl, attrs = _attrs, @@ -161,6 +211,7 @@ py_base = struct( PY_TOOLCHAIN, VENV_TOOLCHAIN, ], + cfg = _python_version_transition ) py_binary = rule( @@ -169,6 +220,7 @@ py_binary = rule( attrs = py_base.attrs, toolchains = py_base.toolchains, executable = True, + cfg = py_base.cfg ) py_test = rule( @@ -177,4 +229,5 @@ py_test = rule( attrs = py_base.attrs, toolchains = py_base.toolchains, test = True, + cfg = py_base.cfg )