refactor: API for deriving customized versions of the base rules (#2610)
This implements a "builder style" API to allow arbitrary modification of
rule, attr, etc
objects used when defining a rule. The net effect is users are able to
use the base
definition for our rules, but define their own with the modifications
they need, without
having to copy/paste portions our implementation, load private files, or
patch source.
The basic way it works is a mutable object ("builder") holds the args
and state that would
be used to create the immutable Bazel object. When `build()` is called,
the immutable
Bazel object (e.g. `attr.string()`) is created. Builders are implemented
for most objects
and their settings (rule, attrs, and supporting objects).
This design is necessary because of three Bazel behaviors:
* attr etc objects are immutable, which means we must keep our own state
* attr etc objects aren't inspectable, which means we must store the
arguments for
creating the immutable objects.
* Starlark objects are frozen after initial bzl file evaluation, which
means creation
of any mutable object must be done at the point of use.
The resulting API resembles the builder APIs common in other languages:
```
r = create_py_binary_rule_builder()
r.attrs.get("srcs").set_mandatory(True)
r.attrs.get("deps").aspects().append(my_aspect)
my_py_binary = r.build()
```
Most objects are thin wrappers for managing a kwargs dict. As such, and
because they're
wrapping a foreign API, they aren't strict in enforcing their internal
state and the
kwargs dict is publicly exposed as an escape hatch.
As of this PR, no public API for e.g. `create_py_binary_rule_builder()`
is exposed. That'll come in a separate PR (to add public access points
under python/api).
Work towards https://github.com/bazelbuild/rules_python/issues/1647diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 0c07002..e19c221 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -103,11 +103,14 @@
"//python/cc:py_cc_toolchain_bzl",
"//python/cc:py_cc_toolchain_info_bzl",
"//python/entry_points:py_console_script_binary_bzl",
+ "//python/private:attr_builders_bzl",
+ "//python/private:builders_util_bzl",
"//python/private:py_binary_rule_bzl",
"//python/private:py_cc_toolchain_rule_bzl",
"//python/private:py_library_rule_bzl",
"//python/private:py_runtime_rule_bzl",
"//python/private:py_test_rule_bzl",
+ "//python/private:rule_builders_bzl",
"//python/private/api:py_common_api_bzl",
"//python/private/pypi:config_settings_bzl",
"//python/private/pypi:pkg_aliases_bzl",
diff --git a/docs/_includes/field_kwargs_doc.md b/docs/_includes/field_kwargs_doc.md
new file mode 100644
index 0000000..0241947
--- /dev/null
+++ b/docs/_includes/field_kwargs_doc.md
@@ -0,0 +1,11 @@
+:::{field} kwargs
+:type: dict[str, Any]
+
+Additional kwargs to use when building. This is to allow manipulations that
+aren't directly supported by the builder's API. The state of this dict
+may or may not reflect prior API calls, and subsequent API calls may
+modify this dict. The general contract is that modifications to this will
+be respected when `build()` is called, assuming there were no API calls
+in between.
+:::
+
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 2928dab..b7e52a3 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -52,9 +52,19 @@
)
bzl_library(
+ name = "attr_builders_bzl",
+ srcs = ["attr_builders.bzl"],
+ deps = [
+ ":builders_util_bzl",
+ "@bazel_skylib//lib:types",
+ ],
+)
+
+bzl_library(
name = "attributes_bzl",
srcs = ["attributes.bzl"],
deps = [
+ ":attr_builders_bzl",
":common_bzl",
":enum_bzl",
":flags_bzl",
@@ -93,6 +103,14 @@
)
bzl_library(
+ name = "builders_util_bzl",
+ srcs = ["builders_util.bzl"],
+ deps = [
+ "@bazel_skylib//lib:types",
+ ],
+)
+
+bzl_library(
name = "bzlmod_enabled_bzl",
srcs = ["bzlmod_enabled.bzl"],
)
@@ -283,6 +301,7 @@
deps = [
":attributes_bzl",
":py_executable_bzl",
+ ":rule_builders_bzl",
":semantics_bzl",
"@bazel_skylib//lib:dicts",
],
@@ -410,6 +429,7 @@
":flags_bzl",
":py_cc_link_params_info_bzl",
":py_internal_bzl",
+ ":rule_builders_bzl",
":toolchain_types_bzl",
"@bazel_skylib//lib:dicts",
"@bazel_skylib//rules:common_settings",
@@ -475,6 +495,7 @@
":py_internal_bzl",
":py_runtime_info_bzl",
":reexports_bzl",
+ ":rule_builders_bzl",
":util_bzl",
"@bazel_skylib//lib:dicts",
"@bazel_skylib//lib:paths",
@@ -515,6 +536,7 @@
":attributes_bzl",
":common_bzl",
":py_executable_bzl",
+ ":rule_builders_bzl",
":semantics_bzl",
"@bazel_skylib//lib:dicts",
],
@@ -564,6 +586,16 @@
)
bzl_library(
+ name = "rule_builders_bzl",
+ srcs = ["rule_builders.bzl"],
+ deps = [
+ ":builders_bzl",
+ ":builders_util_bzl",
+ "@bazel_skylib//lib:types",
+ ],
+)
+
+bzl_library(
name = "semver_bzl",
srcs = ["semver.bzl"],
)
diff --git a/python/private/attr_builders.bzl b/python/private/attr_builders.bzl
new file mode 100644
index 0000000..acd1d40
--- /dev/null
+++ b/python/private/attr_builders.bzl
@@ -0,0 +1,1360 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Builders for creating attributes et al."""
+
+load("@bazel_skylib//lib:types.bzl", "types")
+load(
+ ":builders_util.bzl",
+ "kwargs_getter",
+ "kwargs_getter_doc",
+ "kwargs_getter_mandatory",
+ "kwargs_set_default_doc",
+ "kwargs_set_default_ignore_none",
+ "kwargs_set_default_list",
+ "kwargs_set_default_mandatory",
+ "kwargs_setter",
+ "kwargs_setter_doc",
+ "kwargs_setter_mandatory",
+ "to_label_maybe",
+)
+
+# Various string constants for kwarg key names used across two or more
+# functions, or in contexts with optional lookups (e.g. dict.dict, key in dict).
+# Constants are used to reduce the chance of typos.
+# NOTE: These keys are often part of function signature via `**kwargs`; they
+# are not simply internal names.
+_ALLOW_FILES = "allow_files"
+_ALLOW_EMPTY = "allow_empty"
+_ALLOW_SINGLE_FILE = "allow_single_file"
+_DEFAULT = "default"
+_INPUTS = "inputs"
+_OUTPUTS = "outputs"
+_CFG = "cfg"
+_VALUES = "values"
+
+def _kwargs_set_default_allow_empty(kwargs):
+ existing = kwargs.get(_ALLOW_EMPTY)
+ if existing == None:
+ kwargs[_ALLOW_EMPTY] = True
+
+def _kwargs_getter_allow_empty(kwargs):
+ return kwargs_getter(kwargs, _ALLOW_EMPTY)
+
+def _kwargs_setter_allow_empty(kwargs):
+ return kwargs_setter(kwargs, _ALLOW_EMPTY)
+
+def _kwargs_set_default_allow_files(kwargs):
+ existing = kwargs.get(_ALLOW_FILES)
+ if existing == None:
+ kwargs[_ALLOW_FILES] = False
+
+def _kwargs_getter_allow_files(kwargs):
+ return kwargs_getter(kwargs, _ALLOW_FILES)
+
+def _kwargs_setter_allow_files(kwargs):
+ return kwargs_setter(kwargs, _ALLOW_FILES)
+
+def _kwargs_set_default_aspects(kwargs):
+ kwargs_set_default_list(kwargs, "aspects")
+
+def _kwargs_getter_aspects(kwargs):
+ return kwargs_getter(kwargs, "aspects")
+
+def _kwargs_getter_providers(kwargs):
+ return kwargs_getter(kwargs, "providers")
+
+def _kwargs_set_default_providers(kwargs):
+ kwargs_set_default_list(kwargs, "providers")
+
+def _common_label_build(self, attr_factory):
+ kwargs = dict(self.kwargs)
+ kwargs[_CFG] = self.cfg.build()
+ return attr_factory(**kwargs)
+
+def _WhichCfg_typedef():
+ """Values returned by `AttrCfg.which_cfg`
+
+ :::{field} TARGET
+
+ Indicates the target config is set.
+ :::
+
+ :::{field} EXEC
+
+ Indicates the exec config is set.
+ :::
+ :::{field} NONE
+
+ Indicates the "none" config is set (see {obj}`config.none`).
+ :::
+ :::{field} IMPL
+
+ Indicates a custom transition is set.
+ :::
+ """
+
+# buildifier: disable=name-conventions
+_WhichCfg = struct(
+ TYPEDEF = _WhichCfg_typedef,
+ TARGET = "target",
+ EXEC = "exec",
+ NONE = "none",
+ IMPL = "impl",
+)
+
+def _AttrCfg_typedef():
+ """Builder for `cfg` arg of label attributes.
+
+ :::{function} inputs() -> list[Label]
+ :::
+
+ :::{function} outputs() -> list[Label]
+ :::
+
+ :::{function} which_cfg() -> attrb.WhichCfg
+
+ Tells which of the cfg modes is set. Will be one of: target, exec, none,
+ or implementation
+ :::
+ """
+
+_ATTR_CFG_WHICH = "which"
+_ATTR_CFG_VALUE = "value"
+
+def _AttrCfg_new(
+ inputs = None,
+ outputs = None,
+ **kwargs):
+ """Creates a builder for the `attr.cfg` attribute.
+
+ Args:
+ inputs: {type}`list[Label] | None` inputs to use for a transition
+ outputs: {type}`list[Label] | None` outputs to use for a transition
+ **kwargs: {type}`dict` Three different keyword args are supported.
+ The presence of a keyword arg will mark the respective mode
+ returned by `which_cfg`.
+ - `cfg`: string of either "target" or "exec"
+ - `exec_group`: string of an exec group name to use. None means
+ to use regular exec config (i.e. `config.exec()`)
+ - `implementation`: callable for a custom transition function.
+
+ Returns:
+ {type}`AttrCfg`
+ """
+ state = {
+ _INPUTS: inputs,
+ _OUTPUTS: outputs,
+ # Value depends on _ATTR_CFG_WHICH key. See associated setters.
+ _ATTR_CFG_VALUE: True,
+ # str: one of the _WhichCfg values
+ _ATTR_CFG_WHICH: _WhichCfg.TARGET,
+ }
+ kwargs_set_default_list(state, _INPUTS)
+ kwargs_set_default_list(state, _OUTPUTS)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ _state = state,
+ build = lambda: _AttrCfg_build(self),
+ exec_group = lambda: _AttrCfg_exec_group(self),
+ implementation = lambda: _AttrCfg_implementation(self),
+ inputs = kwargs_getter(state, _INPUTS),
+ none = lambda: _AttrCfg_none(self),
+ outputs = kwargs_getter(state, _OUTPUTS),
+ set_exec = lambda *a, **k: _AttrCfg_set_exec(self, *a, **k),
+ set_implementation = lambda *a, **k: _AttrCfg_set_implementation(self, *a, **k),
+ set_none = lambda: _AttrCfg_set_none(self),
+ set_target = lambda: _AttrCfg_set_target(self),
+ target = lambda: _AttrCfg_target(self),
+ which_cfg = kwargs_getter(state, _ATTR_CFG_WHICH),
+ )
+
+ # Only one of the three kwargs should be present. We just process anything
+ # we see because it's simpler.
+ if _CFG in kwargs:
+ cfg = kwargs.pop(_CFG)
+ if cfg == "target" or cfg == None:
+ self.set_target()
+ elif cfg == "exec":
+ self.set_exec()
+ elif cfg == "none":
+ self.set_none()
+ else:
+ self.set_implementation(cfg)
+ if "exec_group" in kwargs:
+ self.set_exec(kwargs.pop("exec_group"))
+
+ if "implementation" in kwargs:
+ self.set_implementation(kwargs.pop("implementation"))
+
+ return self
+
+def _AttrCfg_from_attr_kwargs_pop(attr_kwargs):
+ """Creates a `AttrCfg` from the cfg arg passed to an attribute bulider.
+
+ Args:
+ attr_kwargs: dict of attr kwargs, it's "cfg" key will be removed.
+
+ Returns:
+ {type}`AttrCfg`
+ """
+ cfg = attr_kwargs.pop(_CFG, None)
+ if not types.is_dict(cfg):
+ kwargs = {_CFG: cfg}
+ else:
+ kwargs = cfg
+ return _AttrCfg_new(**kwargs)
+
+def _AttrCfg_implementation(self):
+ """Tells the custom transition function, if any and applicable.
+
+ Returns:
+ {type}`callable | None` the custom transition function to use, if
+ any, or `None` if a different config mode is being used.
+ """
+ return self._state[_ATTR_CFG_VALUE] if self._state[_ATTR_CFG_WHICH] == _WhichCfg.IMPL else None
+
+def _AttrCfg_none(self):
+ """Tells if none cfg (`config.none()`) is set.
+
+ Returns:
+ {type}`bool` True if none cfg is set, False if not.
+ """
+ return self._state[_ATTR_CFG_VALUE] if self._state[_ATTR_CFG_WHICH] == _WhichCfg.NONE else False
+
+def _AttrCfg_target(self):
+ """Tells if target cfg is set.
+
+ Returns:
+ {type}`bool` True if target cfg is set, False if not.
+ """
+ return self._state[_ATTR_CFG_VALUE] if self._state[_ATTR_CFG_WHICH] == _WhichCfg.TARGET else False
+
+def _AttrCfg_exec_group(self):
+ """Tells the exec group to use if an exec transition is being used.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ {type}`str | None` the name of the exec group to use if any,
+ or `None` if `which_cfg` isn't `exec`
+ """
+ return self._state[_ATTR_CFG_VALUE] if self._state[_ATTR_CFG_WHICH] == _WhichCfg.EXEC else None
+
+def _AttrCfg_set_implementation(self, impl):
+ """Sets a custom transition function to use.
+
+ Args:
+ self: implicitly added.
+ impl: {type}`callable` a transition implementation function.
+ """
+ self._state[_ATTR_CFG_WHICH] = _WhichCfg.IMPL
+ self._state[_ATTR_CFG_VALUE] = impl
+
+def _AttrCfg_set_none(self):
+ """Sets to use the "none" transition."""
+ self._state[_ATTR_CFG_WHICH] = _WhichCfg.NONE
+ self._state[_ATTR_CFG_VALUE] = True
+
+def _AttrCfg_set_exec(self, exec_group = None):
+ """Sets to use an exec transition.
+
+ Args:
+ self: implicitly added.
+ exec_group: {type}`str | None` the exec group name to use, if any.
+ """
+ self._state[_ATTR_CFG_WHICH] = _WhichCfg.EXEC
+ self._state[_ATTR_CFG_VALUE] = exec_group
+
+def _AttrCfg_set_target(self):
+ """Sets to use the target transition."""
+ self._state[_ATTR_CFG_WHICH] = _WhichCfg.TARGET
+ self._state[_ATTR_CFG_VALUE] = True
+
+def _AttrCfg_build(self):
+ which = self._state[_ATTR_CFG_WHICH]
+ value = self._state[_ATTR_CFG_VALUE]
+ if which == None:
+ return None
+ elif which == _WhichCfg.TARGET:
+ # config.target is Bazel 8+
+ if hasattr(config, "target"):
+ return config.target()
+ else:
+ return "target"
+ elif which == _WhichCfg.EXEC:
+ return config.exec(value)
+ elif which == _WhichCfg.NONE:
+ return config.none()
+ elif types.is_function(value):
+ return transition(
+ implementation = value,
+ # Transitions only accept unique lists of strings.
+ inputs = {str(v): None for v in self._state[_INPUTS]}.keys(),
+ outputs = {str(v): None for v in self._state[_OUTPUTS]}.keys(),
+ )
+ else:
+ # Otherwise, just assume the value is valid and whoever set it knows
+ # what they're doing.
+ return value
+
+# buildifier: disable=name-conventions
+AttrCfg = struct(
+ TYPEDEF = _AttrCfg_typedef,
+ new = _AttrCfg_new,
+ # keep sorted
+ exec_group = _AttrCfg_exec_group,
+ implementation = _AttrCfg_implementation,
+ none = _AttrCfg_none,
+ set_exec = _AttrCfg_set_exec,
+ set_implementation = _AttrCfg_set_implementation,
+ set_none = _AttrCfg_set_none,
+ set_target = _AttrCfg_set_target,
+ target = _AttrCfg_target,
+)
+
+def _Bool_typedef():
+ """Builder for attr.bool.
+
+ :::{function} build() -> attr.bool
+ :::
+
+ :::{function} default() -> bool.
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} set_default(v: bool)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+
+ """
+
+def _Bool_new(**kwargs):
+ """Creates a builder for `attr.bool`.
+
+ Args:
+ **kwargs: Same kwargs as {obj}`attr.bool`
+
+ Returns:
+ {type}`Bool`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, False)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ build = lambda: attr.bool(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+Bool = struct(
+ TYPEDEF = _Bool_typedef,
+ new = _Bool_new,
+)
+
+def _Int_typedef():
+ """Builder for attr.int.
+
+ :::{function} build() -> attr.int
+ :::
+
+ :::{function} default() -> int
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} values() -> list[int]
+
+ The returned value is a mutable reference to the underlying list.
+ :::
+
+ :::{function} set_default(v: int)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _Int_new(**kwargs):
+ """Creates a builder for `attr.int`.
+
+ Args:
+ **kwargs: Same kwargs as {obj}`attr.int`
+
+ Returns:
+ {type}`Int`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, 0)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ kwargs_set_default_list(kwargs, _VALUES)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ build = lambda: attr.int(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ values = kwargs_getter(kwargs, _VALUES),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+Int = struct(
+ TYPEDEF = _Int_typedef,
+ new = _Int_new,
+)
+
+def _IntList_typedef():
+ """Builder for attr.int_list.
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.int_list
+ :::
+
+ :::{function} default() -> list[int]
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _IntList_new(**kwargs):
+ """Creates a builder for `attr.int_list`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.int_list`.
+
+ Returns:
+ {type}`IntList`
+ """
+ kwargs_set_default_list(kwargs, _DEFAULT)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ build = lambda: attr.int_list(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+IntList = struct(
+ TYPEDEF = _IntList_typedef,
+ new = _IntList_new,
+)
+
+def _Label_typedef():
+ """Builder for `attr.label` objects.
+
+ :::{function} allow_files() -> bool | list[str] | None
+
+ Note that `allow_files` is mutually exclusive with `allow_single_file`.
+ Only one of the two can have a value set.
+ :::
+
+ :::{function} allow_single_file() -> bool | None
+ Note that `allow_single_file` is mutually exclusive with `allow_files`.
+ Only one of the two can have a value set.
+ :::
+
+ :::{function} aspects() -> list[aspect]
+
+ The returned list is a mutable reference to the underlying list.
+ :::
+
+ :::{function} build() -> attr.label
+ :::
+
+ :::{field} cfg
+ :type: AttrCfg
+ :::
+
+ :::{function} default() -> str | label | configuration_field | None
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} executable() -> bool
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+
+ :::{function} providers() -> list[list[provider]]
+ The returned list is a mutable reference to the underlying list.
+ :::
+
+ :::{function} set_default(v: str | Label)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_executable(v: bool)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _Label_new(**kwargs):
+ """Creates a builder for `attr.label`.
+
+ Args:
+ **kwargs: The same as {obj}`attr.label()`.
+
+ Returns:
+ {type}`Label`
+ """
+ kwargs_set_default_ignore_none(kwargs, "executable", False)
+ _kwargs_set_default_aspects(kwargs)
+ _kwargs_set_default_providers(kwargs)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+
+ kwargs[_DEFAULT] = to_label_maybe(kwargs.get(_DEFAULT))
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ add_allow_files = lambda v: _Label_add_allow_files(self, v),
+ allow_files = _kwargs_getter_allow_files(kwargs),
+ allow_single_file = kwargs_getter(kwargs, _ALLOW_SINGLE_FILE),
+ aspects = _kwargs_getter_aspects(kwargs),
+ build = lambda: _common_label_build(self, attr.label),
+ cfg = _AttrCfg_from_attr_kwargs_pop(kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ executable = kwargs_getter(kwargs, "executable"),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ providers = _kwargs_getter_providers(kwargs),
+ set_allow_files = lambda v: _Label_set_allow_files(self, v),
+ set_allow_single_file = lambda v: _Label_set_allow_single_file(self, v),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_executable = kwargs_setter(kwargs, "executable"),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+def _Label_set_allow_files(self, v):
+ """Set the allow_files arg
+
+ NOTE: Setting `allow_files` unsets `allow_single_file`
+
+ Args:
+ self: implicitly added.
+ v: {type}`bool | list[str] | None` the value to set to.
+ If set to `None`, then `allow_files` is unset.
+ """
+ if v == None:
+ self.kwargs.pop(_ALLOW_FILES, None)
+ else:
+ self.kwargs[_ALLOW_FILES] = v
+ self.kwargs.pop(_ALLOW_SINGLE_FILE, None)
+
+def _Label_add_allow_files(self, *values):
+ """Adds allowed file extensions
+
+ NOTE: Add an allowed file extension unsets `allow_single_file`
+
+ Args:
+ self: implicitly added.
+ *values: {type}`str` file extensions to allow (including dot)
+ """
+ self.kwargs.pop(_ALLOW_SINGLE_FILE, None)
+ if not types.is_list(self.kwargs.get(_ALLOW_FILES)):
+ self.kwargs[_ALLOW_FILES] = []
+ existing = self.kwargs[_ALLOW_FILES]
+ existing.extend([v for v in values if v not in existing])
+
+def _Label_set_allow_single_file(self, v):
+ """Sets the allow_single_file arg.
+
+ NOTE: Setting `allow_single_file` unsets `allow_file`
+
+ Args:
+ self: implicitly added.
+ v: {type}`bool | None` the value to set to.
+ If set to `None`, then `allow_single_file` is unset.
+ """
+ if v == None:
+ self.kwargs.pop(_ALLOW_SINGLE_FILE, None)
+ else:
+ self.kwargs[_ALLOW_SINGLE_FILE] = v
+ self.kwargs.pop(_ALLOW_FILES, None)
+
+# buildifier: disable=name-conventions
+Label = struct(
+ TYPEDEF = _Label_typedef,
+ new = _Label_new,
+ set_allow_files = _Label_set_allow_files,
+ add_allow_files = _Label_add_allow_files,
+ set_allow_single_file = _Label_set_allow_single_file,
+)
+
+def _LabelKeyedStringDict_typedef():
+ """Builder for attr.label_keyed_string_dict.
+
+ :::{function} aspects() -> list[aspect]
+ The returned list is a mutable reference to the underlying list.
+ :::
+
+ :::{function} allow_files() -> bool | list[str]
+ :::
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{field} cfg
+ :type: AttrCfg
+ :::
+
+ :::{function} default() -> dict[str | Label, str] | callable
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} providers() -> list[provider | list[provider]]
+
+ Returns a mutable reference to the underlying list.
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ :::{function} set_allow_empty(v: bool)
+ :::
+ :::{function} set_default(v: dict[str | Label, str] | callable)
+ :::
+ :::{function} set_doc(v: str)
+ :::
+ :::{function} set_allow_files(v: bool | list[str])
+ :::
+ """
+
+def _LabelKeyedStringDict_new(**kwargs):
+ """Creates a builder for `attr.label_keyed_string_dict`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.label_keyed_string_dict`.
+
+ Returns:
+ {type}`LabelKeyedStringDict`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, {})
+ _kwargs_set_default_aspects(kwargs)
+ _kwargs_set_default_providers(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+ _kwargs_set_default_allow_files(kwargs)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ add_allow_files = lambda *v: _LabelKeyedStringDict_add_allow_files(self, *v),
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ allow_files = _kwargs_getter_allow_files(kwargs),
+ aspects = _kwargs_getter_aspects(kwargs),
+ build = lambda: _common_label_build(self, attr.label_keyed_string_dict),
+ cfg = _AttrCfg_from_attr_kwargs_pop(kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ providers = _kwargs_getter_providers(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_allow_files = _kwargs_setter_allow_files(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+def _LabelKeyedStringDict_add_allow_files(self, *values):
+ """Adds allowed file extensions
+
+ Args:
+ self: implicitly added.
+ *values: {type}`str` file extensions to allow (including dot)
+ """
+ if not types.is_list(self.kwargs.get(_ALLOW_FILES)):
+ self.kwargs[_ALLOW_FILES] = []
+ existing = self.kwargs[_ALLOW_FILES]
+ existing.extend([v for v in values if v not in existing])
+
+# buildifier: disable=name-conventions
+LabelKeyedStringDict = struct(
+ TYPEDEF = _LabelKeyedStringDict_typedef,
+ new = _LabelKeyedStringDict_new,
+ add_allow_files = _LabelKeyedStringDict_add_allow_files,
+)
+
+def _LabelList_typedef():
+ """Builder for `attr.label_list`
+
+ :::{function} aspects() -> list[aspect]
+ :::
+
+ :::{function} allow_files() -> bool | list[str]
+ :::
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.label_list
+ :::
+
+ :::{field} cfg
+ :type: AttrCfg
+ :::
+
+ :::{function} default() -> list[str|Label] | configuration_field | callable
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} providers() -> list[provider | list[provider]]
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+
+ :::{function} set_allow_files(v: bool | list[str])
+ :::
+
+ :::{function} set_default(v: list[str|Label] | configuration_field | callable)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _LabelList_new(**kwargs):
+ """Creates a builder for `attr.label_list`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.label_list`.
+
+ Returns:
+ {type}`LabelList`
+ """
+ _kwargs_set_default_allow_empty(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ kwargs_set_default_doc(kwargs)
+ if kwargs.get(_ALLOW_FILES) == None:
+ kwargs[_ALLOW_FILES] = False
+ _kwargs_set_default_aspects(kwargs)
+ kwargs_set_default_list(kwargs, _DEFAULT)
+ _kwargs_set_default_providers(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ allow_files = _kwargs_getter_allow_files(kwargs),
+ aspects = _kwargs_getter_aspects(kwargs),
+ build = lambda: _common_label_build(self, attr.label_list),
+ cfg = _AttrCfg_from_attr_kwargs_pop(kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ providers = _kwargs_getter_providers(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_allow_files = _kwargs_setter_allow_files(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+LabelList = struct(
+ TYPEDEF = _LabelList_typedef,
+ new = _LabelList_new,
+)
+
+def _Output_typedef():
+ """Builder for attr.output
+
+ :::{function} build() -> attr.output
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _Output_new(**kwargs):
+ """Creates a builder for `attr.output`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.output`.
+
+ Returns:
+ {type}`Output`
+ """
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ build = lambda: attr.output(**self.kwargs),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+Output = struct(
+ TYPEDEF = _Output_typedef,
+ new = _Output_new,
+)
+
+def _OutputList_typedef():
+ """Builder for attr.output_list
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.output
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+ :::{function} set_doc(v: str)
+ :::
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _OutputList_new(**kwargs):
+ """Creates a builder for `attr.output_list`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.output_list`.
+
+ Returns:
+ {type}`OutputList`
+ """
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ build = lambda: attr.output_list(**self.kwargs),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+OutputList = struct(
+ TYPEDEF = _OutputList_typedef,
+ new = _OutputList_new,
+)
+
+def _String_typedef():
+ """Builder for `attr.string`
+
+ :::{function} build() -> attr.string
+ :::
+
+ :::{function} default() -> str | configuration_field
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} values() -> list[str]
+ :::
+
+ :::{function} set_default(v: str | configuration_field)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _String_new(**kwargs):
+ """Creates a builder for `attr.string`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.string`.
+
+ Returns:
+ {type}`String`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, "")
+ kwargs_set_default_list(kwargs, _VALUES)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ mandatory = kwargs_getter_mandatory(kwargs),
+ build = lambda: attr.string(**self.kwargs),
+ kwargs = kwargs,
+ values = kwargs_getter(kwargs, _VALUES),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+String = struct(
+ TYPEDEF = _String_typedef,
+ new = _String_new,
+)
+
+def _StringDict_typedef():
+ """Builder for `attr.string_dict`
+
+ :::{function} default() -> dict[str, str]
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.string_dict
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+ :::{function} set_mandatory(v: bool)
+ :::
+ :::{function} set_allow_empty(v: bool)
+ :::
+ """
+
+def _StringDict_new(**kwargs):
+ """Creates a builder for `attr.string_dict`.
+
+ Args:
+ **kwargs: The same args as for `attr.string_dict`.
+
+ Returns:
+ {type}`StringDict`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, {})
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ build = lambda: attr.string_dict(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+StringDict = struct(
+ TYPEDEF = _StringDict_typedef,
+ new = _StringDict_new,
+)
+
+def _StringKeyedLabelDict_typedef():
+ """Builder for attr.string_keyed_label_dict.
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} allow_files() -> bool | list[str]
+ :::
+
+ :::{function} aspects() -> list[aspect]
+ :::
+
+ :::{function} build() -> attr.string_list
+ :::
+
+ :::{field} cfg
+ :type: AttrCfg
+ :::
+
+ :::{function} default() -> dict[str, Label] | callable
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} providers() -> list[list[provider]]
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+
+ :::{function} set_allow_files(v: bool | list[str])
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_default(v: dict[str, Label] | callable)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _StringKeyedLabelDict_new(**kwargs):
+ """Creates a builder for `attr.string_keyed_label_dict`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.string_keyed_label_dict`.
+
+ Returns:
+ {type}`StringKeyedLabelDict`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, {})
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_files(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+ _kwargs_set_default_aspects(kwargs)
+ _kwargs_set_default_providers(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ allow_files = _kwargs_getter_allow_files(kwargs),
+ build = lambda: _common_label_build(self, attr.string_keyed_label_dict),
+ cfg = _AttrCfg_from_attr_kwargs_pop(kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_allow_files = _kwargs_setter_allow_files(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ providers = _kwargs_getter_providers(kwargs),
+ aspects = _kwargs_getter_aspects(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+StringKeyedLabelDict = struct(
+ TYPEDEF = _StringKeyedLabelDict_typedef,
+ new = _StringKeyedLabelDict_new,
+)
+
+def _StringList_typedef():
+ """Builder for `attr.string_list`
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.string_list
+ :::
+
+ :::{field} default
+ :type: Value[list[str] | configuration_field]
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _StringList_new(**kwargs):
+ """Creates a builder for `attr.string_list`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.string_list`.
+
+ Returns:
+ {type}`StringList`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, [])
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ build = lambda: attr.string_list(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+StringList = struct(
+ TYPEDEF = _StringList_typedef,
+ new = _StringList_new,
+)
+
+def _StringListDict_typedef():
+ """Builder for attr.string_list_dict.
+
+ :::{function} allow_empty() -> bool
+ :::
+
+ :::{function} build() -> attr.string_list
+ :::
+
+ :::{function} default() -> dict[str, list[str]]
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} set_allow_empty(v: bool)
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _StringListDict_new(**kwargs):
+ """Creates a builder for `attr.string_list_dict`.
+
+ Args:
+ **kwargs: Same as {obj}`attr.string_list_dict`.
+
+ Returns:
+ {type}`StringListDict`
+ """
+ kwargs_set_default_ignore_none(kwargs, _DEFAULT, {})
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_mandatory(kwargs)
+ _kwargs_set_default_allow_empty(kwargs)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ allow_empty = _kwargs_getter_allow_empty(kwargs),
+ build = lambda: attr.string_list_dict(**self.kwargs),
+ default = kwargs_getter(kwargs, _DEFAULT),
+ doc = kwargs_getter_doc(kwargs),
+ kwargs = kwargs,
+ mandatory = kwargs_getter_mandatory(kwargs),
+ set_allow_empty = _kwargs_setter_allow_empty(kwargs),
+ set_default = kwargs_setter(kwargs, _DEFAULT),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_mandatory = kwargs_setter_mandatory(kwargs),
+ )
+ return self
+
+# buildifier: disable=name-conventions
+StringListDict = struct(
+ TYPEDEF = _StringListDict_typedef,
+ new = _StringListDict_new,
+)
+
+attrb = struct(
+ # keep sorted
+ Bool = _Bool_new,
+ Int = _Int_new,
+ IntList = _IntList_new,
+ Label = _Label_new,
+ LabelKeyedStringDict = _LabelKeyedStringDict_new,
+ LabelList = _LabelList_new,
+ Output = _Output_new,
+ OutputList = _OutputList_new,
+ String = _String_new,
+ StringDict = _StringDict_new,
+ StringKeyedLabelDict = _StringKeyedLabelDict_new,
+ StringList = _StringList_new,
+ StringListDict = _StringListDict_new,
+ WhichCfg = _WhichCfg,
+)
diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl
index e167482..b57e275 100644
--- a/python/private/attributes.bzl
+++ b/python/private/attributes.bzl
@@ -13,14 +13,16 @@
# limitations under the License.
"""Attributes for Python rules."""
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
-load(":common.bzl", "union_attrs")
+load(":attr_builders.bzl", "attrb")
load(":enum.bzl", "enum")
load(":flags.bzl", "PrecompileFlag", "PrecompileSourceRetentionFlag")
load(":py_info.bzl", "PyInfo")
load(":py_internal.bzl", "py_internal")
load(":reexports.bzl", "BuiltinPyInfo")
+load(":rule_builders.bzl", "ruleb")
load(
":semantics.bzl",
"DEPS_ATTR_ALLOW_RULES",
@@ -41,12 +43,18 @@
# NOTE: These are no-op/empty exec groups. If a rule *does* support an exec
# group and needs custom settings, it should merge this dict with one that
# overrides the supported key.
-REQUIRED_EXEC_GROUPS = {
+REQUIRED_EXEC_GROUP_BUILDERS = {
# py_binary may invoke C++ linking, or py rules may be used in combination
# with cc rules (e.g. within the same macro), so support that exec group.
# This exec group is defined by rules_cc for the cc rules.
- "cpp_link": exec_group(),
- "py_precompile": exec_group(),
+ "cpp_link": lambda: ruleb.ExecGroup(),
+ "py_precompile": lambda: ruleb.ExecGroup(),
+}
+
+# Backwards compatibility symbol for Google.
+REQUIRED_EXEC_GROUPS = {
+ k: v().build()
+ for k, v in REQUIRED_EXEC_GROUP_BUILDERS.items()
}
_STAMP_VALUES = [-1, 0, 1]
@@ -139,59 +147,6 @@
is_pyc_collection_enabled = _pyc_collection_attr_is_pyc_collection_enabled,
)
-def create_stamp_attr(**kwargs):
- return {
- "stamp": attr.int(
- values = _STAMP_VALUES,
- doc = """
-Whether to encode build information into the binary. Possible values:
-
-* `stamp = 1`: Always stamp the build information into the binary, even in
- `--nostamp` builds. **This setting should be avoided**, since it potentially kills
- remote caching for the binary and any downstream actions that depend on it.
-* `stamp = 0`: Always replace build information by constant values. This gives
- good build result caching.
-* `stamp = -1`: Embedding of build information is controlled by the
- `--[no]stamp` flag.
-
-Stamped binaries are not rebuilt unless their dependencies change.
-
-WARNING: Stamping can harm build performance by reducing cache hits and should
-be avoided if possible.
-""",
- **kwargs
- ),
- }
-
-def create_srcs_attr(*, mandatory):
- return {
- "srcs": attr.label_list(
- # Google builds change the set of allowed files.
- allow_files = SRCS_ATTR_ALLOW_FILES,
- mandatory = mandatory,
- # Necessary for --compile_one_dependency to work.
- flags = ["DIRECT_COMPILE_TIME_INPUT"],
- doc = """
-The list of Python source files that are processed to create the target. This
-includes all your checked-in code and may include generated source files. The
-`.py` files belong in `srcs` and library targets belong in `deps`. Other binary
-files that may be needed at run time belong in `data`.
-""",
- ),
- }
-
-SRCS_VERSION_ALL_VALUES = ["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"]
-SRCS_VERSION_NON_CONVERSION_VALUES = ["PY2AND3", "PY2ONLY", "PY3ONLY"]
-
-def create_srcs_version_attr(values):
- return {
- "srcs_version": attr.string(
- default = "PY2AND3",
- values = values,
- doc = "Defunct, unused, does nothing.",
- ),
- }
-
def copy_common_binary_kwargs(kwargs):
return {
key: kwargs[key]
@@ -216,7 +171,7 @@
DATA_ATTRS = {
# NOTE: The "flags" attribute is deprecated, but there isn't an alternative
# way to specify that constraints should be ignored.
- "data": attr.label_list(
+ "data": lambda: attrb.LabelList(
allow_files = True,
flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
doc = """
@@ -244,7 +199,7 @@
providers = []
return {
- "_native_rules_allowlist": attr.label(
+ "_native_rules_allowlist": lambda: attrb.Label(
default = default,
providers = providers,
),
@@ -253,7 +208,7 @@
NATIVE_RULES_ALLOWLIST_ATTRS = _create_native_rules_allowlist_attrs()
# Attributes common to all rules.
-COMMON_ATTRS = union_attrs(
+COMMON_ATTRS = dicts.add(
DATA_ATTRS,
NATIVE_RULES_ALLOWLIST_ATTRS,
# buildifier: disable=attr-licenses
@@ -267,11 +222,10 @@
# buildifier: disable=attr-license
"licenses": attr.license() if hasattr(attr, "license") else attr.string_list(),
},
- allow_none = True,
)
IMPORTS_ATTRS = {
- "imports": attr.string_list(
+ "imports": lambda: attrb.StringList(
doc = """
List of import directories to be added to the PYTHONPATH.
@@ -289,9 +243,9 @@
_MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else []
# Attributes common to rules accepting Python sources and deps.
-PY_SRCS_ATTRS = union_attrs(
+PY_SRCS_ATTRS = dicts.add(
{
- "deps": attr.label_list(
+ "deps": lambda: attrb.LabelList(
providers = [
[PyInfo],
[CcInfo],
@@ -310,7 +264,7 @@
attribute.
""",
),
- "precompile": attr.string(
+ "precompile": lambda: attrb.String(
doc = """
Whether py source files **for this target** should be precompiled.
@@ -332,7 +286,7 @@
default = PrecompileAttr.INHERIT,
values = sorted(PrecompileAttr.__members__.values()),
),
- "precompile_invalidation_mode": attr.string(
+ "precompile_invalidation_mode": lambda: attrb.String(
doc = """
How precompiled files should be verified to be up-to-date with their associated
source files. Possible values are:
@@ -350,7 +304,7 @@
default = PrecompileInvalidationModeAttr.AUTO,
values = sorted(PrecompileInvalidationModeAttr.__members__.values()),
),
- "precompile_optimize_level": attr.int(
+ "precompile_optimize_level": lambda: attrb.Int(
doc = """
The optimization level for precompiled files.
@@ -363,7 +317,7 @@
""",
default = 0,
),
- "precompile_source_retention": attr.string(
+ "precompile_source_retention": lambda: attrb.String(
default = PrecompileSourceRetentionAttr.INHERIT,
values = sorted(PrecompileSourceRetentionAttr.__members__.values()),
doc = """
@@ -375,7 +329,7 @@
* `omit_source`: Don't include the original py source.
""",
),
- "pyi_deps": attr.label_list(
+ "pyi_deps": lambda: attrb.LabelList(
doc = """
Dependencies providing type definitions the library needs.
@@ -391,7 +345,7 @@
[CcInfo],
] + _MaybeBuiltinPyInfo,
),
- "pyi_srcs": attr.label_list(
+ "pyi_srcs": lambda: attrb.LabelList(
doc = """
Type definition files for the library.
@@ -404,37 +358,61 @@
""",
allow_files = True,
),
- # Required attribute, but details vary by rule.
- # Use create_srcs_attr to create one.
- "srcs": None,
- # NOTE: In Google, this attribute is deprecated, and can only
- # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute
- # has a separate story.
- # Required attribute, but the details vary by rule.
- # Use create_srcs_version_attr to create one.
- "srcs_version": None,
- "_precompile_flag": attr.label(
+ "srcs": lambda: attrb.LabelList(
+ # Google builds change the set of allowed files.
+ allow_files = SRCS_ATTR_ALLOW_FILES,
+ # Necessary for --compile_one_dependency to work.
+ flags = ["DIRECT_COMPILE_TIME_INPUT"],
+ doc = """
+The list of Python source files that are processed to create the target. This
+includes all your checked-in code and may include generated source files. The
+`.py` files belong in `srcs` and library targets belong in `deps`. Other binary
+files that may be needed at run time belong in `data`.
+""",
+ ),
+ "srcs_version": lambda: attrb.String(
+ doc = "Defunct, unused, does nothing.",
+ ),
+ "_precompile_flag": lambda: attrb.Label(
default = "//python/config_settings:precompile",
providers = [BuildSettingInfo],
),
- "_precompile_source_retention_flag": attr.label(
+ "_precompile_source_retention_flag": lambda: attrb.Label(
default = "//python/config_settings:precompile_source_retention",
providers = [BuildSettingInfo],
),
# Force enabling auto exec groups, see
# https://bazel.build/extending/auto-exec-groups#how-enable-particular-rule
- "_use_auto_exec_groups": attr.bool(default = True),
+ "_use_auto_exec_groups": lambda: attrb.Bool(
+ default = True,
+ ),
},
- allow_none = True,
)
+COVERAGE_ATTRS = {
+ # Magic attribute to help C++ coverage work. There's no
+ # docs about this; see TestActionBuilder.java
+ "_collect_cc_coverage": lambda: attrb.Label(
+ default = "@bazel_tools//tools/test:collect_cc_coverage",
+ executable = True,
+ cfg = "exec",
+ ),
+ # Magic attribute to make coverage work. There's no
+ # docs about this; see TestActionBuilder.java
+ "_lcov_merger": lambda: attrb.Label(
+ default = configuration_field(fragment = "coverage", name = "output_generator"),
+ executable = True,
+ cfg = "exec",
+ ),
+}
+
# Attributes specific to Python executable-equivalent rules. Such rules may not
# accept Python sources (e.g. some packaged-version of a py_test/py_binary), but
# still accept Python source-agnostic settings.
-AGNOSTIC_EXECUTABLE_ATTRS = union_attrs(
+AGNOSTIC_EXECUTABLE_ATTRS = dicts.add(
DATA_ATTRS,
{
- "env": attr.string_dict(
+ "env": lambda: attrb.StringDict(
doc = """\
Dictionary of strings; optional; values are subject to `$(location)` and "Make
variable" substitution.
@@ -443,22 +421,40 @@
`test` or `run`.
""",
),
- # The value is required, but varies by rule and/or rule type. Use
- # create_stamp_attr to create one.
- "stamp": None,
+ "stamp": lambda: attrb.Int(
+ values = _STAMP_VALUES,
+ doc = """
+Whether to encode build information into the binary. Possible values:
+
+* `stamp = 1`: Always stamp the build information into the binary, even in
+ `--nostamp` builds. **This setting should be avoided**, since it potentially kills
+ remote caching for the binary and any downstream actions that depend on it.
+* `stamp = 0`: Always replace build information by constant values. This gives
+ good build result caching.
+* `stamp = -1`: Embedding of build information is controlled by the
+ `--[no]stamp` flag.
+
+Stamped binaries are not rebuilt unless their dependencies change.
+
+WARNING: Stamping can harm build performance by reducing cache hits and should
+be avoided if possible.
+""",
+ default = -1,
+ ),
},
- allow_none = True,
)
-# Attributes specific to Python test-equivalent executable rules. Such rules may
-# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
-# but still accept Python source-agnostic settings.
-AGNOSTIC_TEST_ATTRS = union_attrs(
- AGNOSTIC_EXECUTABLE_ATTRS,
+def _init_agnostic_test_attrs():
+ base_stamp = AGNOSTIC_EXECUTABLE_ATTRS["stamp"]
+
# Tests have stamping disabled by default.
- create_stamp_attr(default = 0),
- {
- "env_inherit": attr.string_list(
+ def stamp_default_disabled():
+ b = base_stamp()
+ b.set_default(0)
+ return b
+
+ return dicts.add(AGNOSTIC_EXECUTABLE_ATTRS, {
+ "env_inherit": lambda: attrb.StringList(
doc = """\
List of strings; optional
@@ -466,8 +462,9 @@
environment when the test is executed by bazel test.
""",
),
+ "stamp": stamp_default_disabled,
# TODO(b/176993122): Remove when Bazel automatically knows to run on darwin.
- "_apple_constraints": attr.label_list(
+ "_apple_constraints": lambda: attrb.LabelList(
default = [
"@platforms//os:ios",
"@platforms//os:macos",
@@ -476,16 +473,17 @@
"@platforms//os:watchos",
],
),
- },
-)
+ })
+
+# Attributes specific to Python test-equivalent executable rules. Such rules may
+# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
+# but still accept Python source-agnostic settings.
+AGNOSTIC_TEST_ATTRS = _init_agnostic_test_attrs()
# Attributes specific to Python binary-equivalent executable rules. Such rules may
# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
# but still accept Python source-agnostic settings.
-AGNOSTIC_BINARY_ATTRS = union_attrs(
- AGNOSTIC_EXECUTABLE_ATTRS,
- create_stamp_attr(default = -1),
-)
+AGNOSTIC_BINARY_ATTRS = dicts.add(AGNOSTIC_EXECUTABLE_ATTRS)
# Attribute names common to all Python rules
COMMON_ATTR_NAMES = [
diff --git a/python/private/builders.bzl b/python/private/builders.bzl
index bf5dbb8..50aa3ed 100644
--- a/python/private/builders.bzl
+++ b/python/private/builders.bzl
@@ -96,145 +96,6 @@
kwargs["order"] = self._order[0]
return depset(direct = self.direct, transitive = self.transitive, **kwargs)
-def _Optional(*initial):
- """A wrapper for a re-assignable value that may or may not be set.
-
- This allows structs to have attributes that aren't inherently mutable
- and must be re-assigned to have their value updated.
-
- Args:
- *initial: A single vararg to be the initial value, or no args
- to leave it unset.
-
- Returns:
- {type}`Optional`
- """
- if len(initial) > 1:
- fail("Only zero or one positional arg allowed")
-
- # buildifier: disable=uninitialized
- self = struct(
- _value = list(initial),
- present = lambda *a, **k: _Optional_present(self, *a, **k),
- set = lambda *a, **k: _Optional_set(self, *a, **k),
- get = lambda *a, **k: _Optional_get(self, *a, **k),
- )
- return self
-
-def _Optional_set(self, value):
- """Sets the value of the optional.
-
- Args:
- self: implicitly added
- value: the value to set.
- """
- if len(self._value) == 0:
- self._value.append(value)
- else:
- self._value[0] = value
-
-def _Optional_get(self):
- """Gets the value of the optional, or error.
-
- Args:
- self: implicitly added
-
- Returns:
- The stored value, or error if not set.
- """
- if not len(self._value):
- fail("Value not present")
- return self._value[0]
-
-def _Optional_present(self):
- """Tells if a value is present.
-
- Args:
- self: implicitly added
-
- Returns:
- {type}`bool` True if the value is set, False if not.
- """
- return len(self._value) > 0
-
-def _RuleBuilder(implementation = None, **kwargs):
- """Builder for creating rules.
-
- Args:
- implementation: {type}`callable` The rule implementation function.
- **kwargs: The same as the `rule()` function, but using builders
- for the non-mutable Bazel objects.
- """
-
- # buildifier: disable=uninitialized
- self = struct(
- attrs = dict(kwargs.pop("attrs", None) or {}),
- cfg = kwargs.pop("cfg", None) or _TransitionBuilder(),
- exec_groups = dict(kwargs.pop("exec_groups", None) or {}),
- executable = _Optional(),
- fragments = list(kwargs.pop("fragments", None) or []),
- implementation = _Optional(implementation),
- extra_kwargs = kwargs,
- provides = list(kwargs.pop("provides", None) or []),
- test = _Optional(),
- toolchains = list(kwargs.pop("toolchains", None) or []),
- build = lambda *a, **k: _RuleBuilder_build(self, *a, **k),
- to_kwargs = lambda *a, **k: _RuleBuilder_to_kwargs(self, *a, **k),
- )
- if "test" in kwargs:
- self.test.set(kwargs.pop("test"))
- if "executable" in kwargs:
- self.executable.set(kwargs.pop("executable"))
- return self
-
-def _RuleBuilder_build(self, debug = ""):
- """Builds a `rule` object
-
- Args:
- self: implicitly added
- debug: {type}`str` If set, prints the args used to create the rule.
-
- Returns:
- {type}`rule`
- """
- kwargs = self.to_kwargs()
- if debug:
- lines = ["=" * 80, "rule kwargs: {}:".format(debug)]
- for k, v in sorted(kwargs.items()):
- lines.append(" {}={}".format(k, v))
- print("\n".join(lines)) # buildifier: disable=print
- return rule(**kwargs)
-
-def _RuleBuilder_to_kwargs(self):
- """Builds the arguments for calling `rule()`.
-
- Args:
- self: implicitly added
-
- Returns:
- {type}`dict`
- """
- kwargs = {}
- if self.executable.present():
- kwargs["executable"] = self.executable.get()
- if self.test.present():
- kwargs["test"] = self.test.get()
-
- kwargs.update(
- implementation = self.implementation.get(),
- cfg = self.cfg.build() if self.cfg.implementation.present() else None,
- attrs = {
- k: (v.build() if hasattr(v, "build") else v)
- for k, v in self.attrs.items()
- },
- exec_groups = self.exec_groups,
- fragments = self.fragments,
- provides = self.provides,
- toolchains = self.toolchains,
- )
- kwargs.update(self.extra_kwargs)
- return kwargs
-
def _RunfilesBuilder():
"""Creates a `RunfilesBuilder`.
@@ -316,91 +177,6 @@
**kwargs
).merge_all(self.runfiles)
-def _SetBuilder(initial = None):
- """Builder for list of unique values.
-
- Args:
- initial: {type}`list | None` The initial values.
-
- Returns:
- {type}`SetBuilder`
- """
- initial = {} if not initial else {v: None for v in initial}
-
- # buildifier: disable=uninitialized
- self = struct(
- # TODO - Switch this to use set() builtin when available
- # https://bazel.build/rules/lib/core/set
- _values = initial,
- update = lambda *a, **k: _SetBuilder_update(self, *a, **k),
- build = lambda *a, **k: _SetBuilder_build(self, *a, **k),
- )
- return self
-
-def _SetBuilder_build(self):
- """Builds the values into a list
-
- Returns:
- {type}`list`
- """
- return self._values.keys()
-
-def _SetBuilder_update(self, *others):
- """Adds values to the builder.
-
- Args:
- self: implicitly added
- *others: {type}`list` values to add to the set.
- """
- for other in others:
- for value in other:
- if value not in self._values:
- self._values[value] = None
-
-def _TransitionBuilder(implementation = None, inputs = None, outputs = None, **kwargs):
- """Builder for transition objects.
-
- Args:
- implementation: {type}`callable` the transition implementation function.
- inputs: {type}`list[str]` the inputs for the transition.
- outputs: {type}`list[str]` the outputs of the transition.
- **kwargs: Extra keyword args to use when building.
-
- Returns:
- {type}`TransitionBuilder`
- """
-
- # buildifier: disable=uninitialized
- self = struct(
- implementation = _Optional(implementation),
- # Bazel requires transition.inputs to have unique values, so use set
- # semantics so extenders of a transition can easily add/remove values.
- # TODO - Use set builtin instead of custom builder, when available.
- # https://bazel.build/rules/lib/core/set
- inputs = _SetBuilder(inputs),
- # Bazel requires transition.inputs to have unique values, so use set
- # semantics so extenders of a transition can easily add/remove values.
- # TODO - Use set builtin instead of custom builder, when available.
- # https://bazel.build/rules/lib/core/set
- outputs = _SetBuilder(outputs),
- extra_kwargs = kwargs,
- build = lambda *a, **k: _TransitionBuilder_build(self, *a, **k),
- )
- return self
-
-def _TransitionBuilder_build(self):
- """Creates a transition from the builder.
-
- Returns:
- {type}`transition`
- """
- return transition(
- implementation = self.implementation.get(),
- inputs = self.inputs.build(),
- outputs = self.outputs.build(),
- **self.extra_kwargs
- )
-
# Skylib's types module doesn't have is_file, so roll our own
def _is_file(value):
return type(value) == "File"
@@ -411,8 +187,4 @@
builders = struct(
DepsetBuilder = _DepsetBuilder,
RunfilesBuilder = _RunfilesBuilder,
- RuleBuilder = _RuleBuilder,
- TransitionBuilder = _TransitionBuilder,
- SetBuilder = _SetBuilder,
- Optional = _Optional,
)
diff --git a/python/private/builders_util.bzl b/python/private/builders_util.bzl
new file mode 100644
index 0000000..139084f
--- /dev/null
+++ b/python/private/builders_util.bzl
@@ -0,0 +1,116 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for builders."""
+
+load("@bazel_skylib//lib:types.bzl", "types")
+
+def to_label_maybe(value):
+ """Converts `value` to a `Label`, maybe.
+
+ The "maybe" qualification is because invalid values for `Label()`
+ are returned as-is (e.g. None, or special values that might be
+ used with e.g. the `default` attribute arg).
+
+ Args:
+ value: {type}`str | Label | None | object` the value to turn into a label,
+ or return as-is.
+
+ Returns:
+ {type}`Label | input_value`
+ """
+ if value == None:
+ return None
+ if is_label(value):
+ return value
+ if types.is_string(value):
+ return Label(value)
+ return value
+
+def is_label(obj):
+ """Tell if an object is a `Label`."""
+ return type(obj) == "Label"
+
+def kwargs_set_default_ignore_none(kwargs, key, default):
+ """Normalize None/missing to `default`."""
+ existing = kwargs.get(key)
+ if existing == None:
+ kwargs[key] = default
+
+def kwargs_set_default_list(kwargs, key):
+ """Normalizes None/missing to list."""
+ existing = kwargs.get(key)
+ if existing == None:
+ kwargs[key] = []
+
+def kwargs_set_default_dict(kwargs, key):
+ """Normalizes None/missing to list."""
+ existing = kwargs.get(key)
+ if existing == None:
+ kwargs[key] = {}
+
+def kwargs_set_default_doc(kwargs):
+ """Sets the `doc` arg default."""
+ existing = kwargs.get("doc")
+ if existing == None:
+ kwargs["doc"] = ""
+
+def kwargs_set_default_mandatory(kwargs):
+ """Sets `False` as the `mandatory` arg default."""
+ existing = kwargs.get("mandatory")
+ if existing == None:
+ kwargs["mandatory"] = False
+
+def kwargs_getter(kwargs, key):
+ """Create a function to get `key` from `kwargs`."""
+ return lambda: kwargs.get(key)
+
+def kwargs_setter(kwargs, key):
+ """Create a function to set `key` in `kwargs`."""
+
+ def setter(v):
+ kwargs[key] = v
+
+ return setter
+
+def kwargs_getter_doc(kwargs):
+ """Creates a `kwargs_getter` for the `doc` key."""
+ return kwargs_getter(kwargs, "doc")
+
+def kwargs_setter_doc(kwargs):
+ """Creates a `kwargs_setter` for the `doc` key."""
+ return kwargs_setter(kwargs, "doc")
+
+def kwargs_getter_mandatory(kwargs):
+ """Creates a `kwargs_getter` for the `mandatory` key."""
+ return kwargs_getter(kwargs, "mandatory")
+
+def kwargs_setter_mandatory(kwargs):
+ """Creates a `kwargs_setter` for the `mandatory` key."""
+ return kwargs_setter(kwargs, "mandatory")
+
+def list_add_unique(add_to, others):
+ """Bulk add values to a list if not already present.
+
+ Args:
+ add_to: {type}`list[T]` the list to add values to. It is modified
+ in-place.
+ others: {type}`collection[collection[T]]` collection of collections of
+ the values to add.
+ """
+ existing = {v: None for v in add_to}
+ for values in others:
+ for value in values:
+ if value not in existing:
+ add_to.append(value)
diff --git a/python/private/common.bzl b/python/private/common.bzl
index 137f0d2..48e2653 100644
--- a/python/private/common.bzl
+++ b/python/private/common.bzl
@@ -208,52 +208,6 @@
extra_runfiles = extra_runfiles,
)
-def union_attrs(*attr_dicts, allow_none = False):
- """Helper for combining and building attriute dicts for rules.
-
- Similar to dict.update, except:
- * Duplicate keys raise an error if they aren't equal. This is to prevent
- unintentionally replacing an attribute with a potentially incompatible
- definition.
- * None values are special: They mean the attribute is required, but the
- value should be provided by another attribute dict (depending on the
- `allow_none` arg).
- Args:
- *attr_dicts: The dicts to combine.
- allow_none: bool, if True, then None values are allowed. If False,
- then one of `attrs_dicts` must set a non-None value for keys
- with a None value.
-
- Returns:
- dict of attributes.
- """
- result = {}
- missing = {}
- for attr_dict in attr_dicts:
- for attr_name, value in attr_dict.items():
- if value == None and not allow_none:
- if attr_name not in result:
- missing[attr_name] = None
- else:
- if attr_name in missing:
- missing.pop(attr_name)
-
- if attr_name not in result or result[attr_name] == None:
- result[attr_name] = value
- elif value != None and result[attr_name] != value:
- fail("Duplicate attribute name: '{}': existing={}, new={}".format(
- attr_name,
- result[attr_name],
- value,
- ))
-
- # Else, they're equal, so do nothing. This allows merging dicts
- # that both define the same key from a common place.
-
- if missing and not allow_none:
- fail("Required attributes missing: " + csv(missing.keys()))
- return result
-
def csv(values):
"""Convert a list of strings to comma separated value string."""
return ", ".join(sorted(values))
diff --git a/python/private/py_binary_rule.bzl b/python/private/py_binary_rule.bzl
index 5b40f52..0e1912c 100644
--- a/python/private/py_binary_rule.bzl
+++ b/python/private/py_binary_rule.bzl
@@ -20,23 +20,6 @@
"py_executable_impl",
)
-_COVERAGE_ATTRS = {
- # Magic attribute to help C++ coverage work. There's no
- # docs about this; see TestActionBuilder.java
- "_collect_cc_coverage": attr.label(
- default = "@bazel_tools//tools/test:collect_cc_coverage",
- executable = True,
- cfg = "exec",
- ),
- # Magic attribute to make coverage work. There's no
- # docs about this; see TestActionBuilder.java
- "_lcov_merger": attr.label(
- default = configuration_field(fragment = "coverage", name = "output_generator"),
- executable = True,
- cfg = "exec",
- ),
-}
-
def _py_binary_impl(ctx):
return py_executable_impl(
ctx = ctx,
@@ -50,7 +33,6 @@
executable = True,
)
builder.attrs.update(AGNOSTIC_BINARY_ATTRS)
- builder.attrs.update(_COVERAGE_ATTRS)
return builder
py_binary = create_binary_rule_builder().build()
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index a2ccdc6..f85f242 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -18,18 +18,17 @@
load("@bazel_skylib//lib:structs.bzl", "structs")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
+load(":attr_builders.bzl", "attrb")
load(
":attributes.bzl",
"AGNOSTIC_EXECUTABLE_ATTRS",
"COMMON_ATTRS",
+ "COVERAGE_ATTRS",
"IMPORTS_ATTRS",
"PY_SRCS_ATTRS",
"PrecompileAttr",
"PycCollectionAttr",
- "REQUIRED_EXEC_GROUPS",
- "SRCS_VERSION_ALL_VALUES",
- "create_srcs_attr",
- "create_srcs_version_attr",
+ "REQUIRED_EXEC_GROUP_BUILDERS",
)
load(":builders.bzl", "builders")
load(":cc_helper.bzl", "cc_helper")
@@ -50,7 +49,6 @@
"is_bool",
"runfiles_root_path",
"target_platform_has_any_constraint",
- "union_attrs",
)
load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag")
load(":precompile.bzl", "maybe_precompile")
@@ -60,6 +58,7 @@
load(":py_internal.bzl", "py_internal")
load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo")
load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
+load(":rule_builders.bzl", "ruleb")
load(
":semantics.bzl",
"ALLOWED_MAIN_EXTENSIONS",
@@ -79,21 +78,16 @@
_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
_PYTHON_VERSION_FLAG = str(Label("//python/config_settings:python_version"))
-# Bazel 5.4 doesn't have config_common.toolchain_type
-_CC_TOOLCHAINS = [config_common.toolchain_type(
- "@bazel_tools//tools/cpp:toolchain_type",
- mandatory = False,
-)] if hasattr(config_common, "toolchain_type") else []
-
# Non-Google-specific attributes for executables
# These attributes are for rules that accept Python sources.
-EXECUTABLE_ATTRS = union_attrs(
+EXECUTABLE_ATTRS = dicts.add(
COMMON_ATTRS,
AGNOSTIC_EXECUTABLE_ATTRS,
PY_SRCS_ATTRS,
IMPORTS_ATTRS,
+ COVERAGE_ATTRS,
{
- "legacy_create_init": attr.int(
+ "legacy_create_init": lambda: attrb.Int(
default = -1,
values = [-1, 0, 1],
doc = """\
@@ -110,7 +104,7 @@
# label, it is more treated as a string, and doesn't have to refer to
# anything that exists because it gets treated as suffix-search string
# over `srcs`.
- "main": attr.label(
+ "main": lambda: attrb.Label(
allow_single_file = True,
doc = """\
Optional; the name of the source file that is the main entry point of the
@@ -119,7 +113,7 @@
filename in `srcs`, `main` must be specified.
""",
),
- "pyc_collection": attr.string(
+ "pyc_collection": lambda: attrb.String(
default = PycCollectionAttr.INHERIT,
values = sorted(PycCollectionAttr.__members__.values()),
doc = """
@@ -134,7 +128,7 @@
target level.
""",
),
- "python_version": attr.string(
+ "python_version": lambda: attrb.String(
# TODO(b/203567235): In the Java impl, the default comes from
# --python_version. Not clear what the Starlark equivalent is.
doc = """
@@ -160,25 +154,25 @@
""",
),
# Required to opt-in to the transition feature.
- "_allowlist_function_transition": attr.label(
+ "_allowlist_function_transition": lambda: attrb.Label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
- "_bootstrap_impl_flag": attr.label(
+ "_bootstrap_impl_flag": lambda: attrb.Label(
default = "//python/config_settings:bootstrap_impl",
providers = [BuildSettingInfo],
),
- "_bootstrap_template": attr.label(
+ "_bootstrap_template": lambda: attrb.Label(
allow_single_file = True,
default = "@bazel_tools//tools/python:python_bootstrap_template.txt",
),
- "_launcher": attr.label(
+ "_launcher": lambda: attrb.Label(
cfg = "target",
# NOTE: This is an executable, but is only used for Windows. It
# can't have executable=True because the backing target is an
# empty target for other platforms.
default = "//tools/launcher:launcher",
),
- "_py_interpreter": attr.label(
+ "_py_interpreter": lambda: attrb.Label(
# The configuration_field args are validated when called;
# we use the precense of py_internal to indicate this Bazel
# build has that fragment and name.
@@ -193,32 +187,29 @@
"_py_toolchain_type": attr.label(
default = TARGET_TOOLCHAIN_TYPE,
),
- "_python_version_flag": attr.label(
+ "_python_version_flag": lambda: attrb.Label(
default = "//python/config_settings:python_version",
),
- "_venvs_use_declare_symlink_flag": attr.label(
+ "_venvs_use_declare_symlink_flag": lambda: attrb.Label(
default = "//python/config_settings:venvs_use_declare_symlink",
providers = [BuildSettingInfo],
),
- "_windows_constraints": attr.label_list(
+ "_windows_constraints": lambda: attrb.LabelList(
default = [
"@platforms//os:windows",
],
),
- "_windows_launcher_maker": attr.label(
+ "_windows_launcher_maker": lambda: attrb.Label(
default = "@bazel_tools//tools/launcher:launcher_maker",
cfg = "exec",
executable = True,
),
- "_zipper": attr.label(
+ "_zipper": lambda: attrb.Label(
cfg = "exec",
executable = True,
default = "@bazel_tools//tools/zip:zipper",
),
},
- create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
- create_srcs_attr(mandatory = True),
- allow_none = True,
)
def convert_legacy_create_init_to_int(kwargs):
@@ -1747,23 +1738,25 @@
return create_executable_rule_builder().build()
def create_executable_rule_builder(implementation, **kwargs):
- builder = builders.RuleBuilder(
+ builder = ruleb.Rule(
implementation = implementation,
attrs = EXECUTABLE_ATTRS,
- exec_groups = REQUIRED_EXEC_GROUPS,
+ exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), # Mutable copy
fragments = ["py", "bazel_py"],
provides = [PyExecutableInfo],
toolchains = [
- TOOLCHAIN_TYPE,
- config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
- ] + _CC_TOOLCHAINS,
- cfg = builders.TransitionBuilder(
+ ruleb.ToolchainType(TOOLCHAIN_TYPE),
+ ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
+ ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
+ ],
+ cfg = dict(
implementation = _transition_executable_impl,
inputs = [_PYTHON_VERSION_FLAG],
outputs = [_PYTHON_VERSION_FLAG],
),
**kwargs
)
+ builder.attrs.get("srcs").set_mandatory(True)
return builder
def cc_configure_features(
diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl
index 350ea35..a774104 100644
--- a/python/private/py_library.bzl
+++ b/python/private/py_library.bzl
@@ -15,16 +15,14 @@
load("@bazel_skylib//lib:dicts.bzl", "dicts")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+load(":attr_builders.bzl", "attrb")
load(
":attributes.bzl",
"COMMON_ATTRS",
"IMPORTS_ATTRS",
"PY_SRCS_ATTRS",
"PrecompileAttr",
- "REQUIRED_EXEC_GROUPS",
- "SRCS_VERSION_ALL_VALUES",
- "create_srcs_attr",
- "create_srcs_version_attr",
+ "REQUIRED_EXEC_GROUP_BUILDERS",
)
load(":builders.bzl", "builders")
load(
@@ -35,11 +33,11 @@
"create_output_group_info",
"create_py_info",
"filter_to_py_srcs",
- "union_attrs",
)
load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag")
load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
load(":py_internal.bzl", "py_internal")
+load(":rule_builders.bzl", "ruleb")
load(
":toolchain_types.bzl",
"EXEC_TOOLS_TOOLCHAIN_TYPE",
@@ -48,14 +46,12 @@
_py_builtins = py_internal
-LIBRARY_ATTRS = union_attrs(
+LIBRARY_ATTRS = dicts.add(
COMMON_ATTRS,
PY_SRCS_ATTRS,
IMPORTS_ATTRS,
- create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
- create_srcs_attr(mandatory = False),
{
- "_add_srcs_to_runfiles_flag": attr.label(
+ "_add_srcs_to_runfiles_flag": lambda: attrb.Label(
default = "//python/config_settings:add_srcs_to_runfiles",
),
},
@@ -145,14 +141,15 @@
:::
"""
-def create_py_library_rule(*, attrs = {}, **kwargs):
+def create_py_library_rule_builder(*, attrs = {}, **kwargs):
"""Creates a py_library rule.
Args:
attrs: dict of rule attributes.
- **kwargs: Additional kwargs to pass onto the rule() call.
+ **kwargs: Additional kwargs to pass onto {obj}`ruleb.Rule()`.
+
Returns:
- A rule object
+ {type}`ruleb.Rule` builder object.
"""
# Within Google, the doc attribute is overridden
@@ -161,13 +158,15 @@
# TODO: b/253818097 - fragments=py is only necessary so that
# RequiredConfigFragmentsTest passes
fragments = kwargs.pop("fragments", None) or []
- kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
- return rule(
+ kwargs["exec_groups"] = REQUIRED_EXEC_GROUP_BUILDERS | (kwargs.get("exec_groups") or {})
+
+ builder = ruleb.Rule(
attrs = dicts.add(LIBRARY_ATTRS, attrs),
- toolchains = [
- config_common.toolchain_type(TOOLCHAIN_TYPE, mandatory = False),
- config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
- ],
fragments = fragments + ["py"],
+ toolchains = [
+ ruleb.ToolchainType(TOOLCHAIN_TYPE, mandatory = False),
+ ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
+ ],
**kwargs
)
+ return builder
diff --git a/python/private/py_library_rule.bzl b/python/private/py_library_rule.bzl
index 8a8d6cf..44382a7 100644
--- a/python/private/py_library_rule.bzl
+++ b/python/private/py_library_rule.bzl
@@ -15,7 +15,7 @@
load(":common.bzl", "collect_cc_info", "create_library_semantics_struct", "get_imports")
load(":precompile.bzl", "maybe_precompile")
-load(":py_library.bzl", "create_py_library_rule", "py_library_impl")
+load(":py_library.bzl", "create_py_library_rule_builder", "py_library_impl")
def _py_library_impl_with_semantics(ctx):
return py_library_impl(
@@ -27,6 +27,6 @@
),
)
-py_library = create_py_library_rule(
+py_library = create_py_library_rule_builder(
implementation = _py_library_impl_with_semantics,
-)
+).build()
diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl
index 5ce8161..9407cac 100644
--- a/python/private/py_runtime_rule.bzl
+++ b/python/private/py_runtime_rule.bzl
@@ -188,19 +188,21 @@
```
""",
fragments = ["py"],
- attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, {
- "abi_flags": attr.string(
- default = "<AUTO>",
- doc = """
+ attrs = dicts.add(
+ {k: v().build() for k, v in NATIVE_RULES_ALLOWLIST_ATTRS.items()},
+ {
+ "abi_flags": attr.string(
+ default = "<AUTO>",
+ doc = """
The runtime's ABI flags, i.e. `sys.abiflags`.
If not set, then it will be set based on flags.
""",
- ),
- "bootstrap_template": attr.label(
- allow_single_file = True,
- default = DEFAULT_BOOTSTRAP_TEMPLATE,
- doc = """
+ ),
+ "bootstrap_template": attr.label(
+ allow_single_file = True,
+ default = DEFAULT_BOOTSTRAP_TEMPLATE,
+ doc = """
The bootstrap script template file to use. Should have %python_binary%,
%workspace_name%, %main%, and %imports%.
@@ -218,10 +220,10 @@
See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables.
""",
- ),
- "coverage_tool": attr.label(
- allow_files = False,
- doc = """
+ ),
+ "coverage_tool": attr.label(
+ allow_files = False,
+ doc = """
This is a target to use for collecting code coverage information from
{rule}`py_binary` and {rule}`py_test` targets.
@@ -235,25 +237,25 @@
of [`coverage.py`](https://coverage.readthedocs.io), at least including
the `run` and `lcov` subcommands.
""",
- ),
- "files": attr.label_list(
- allow_files = True,
- doc = """
+ ),
+ "files": attr.label_list(
+ allow_files = True,
+ doc = """
For an in-build runtime, this is the set of files comprising this runtime.
These files will be added to the runfiles of Python binaries that use this
runtime. For a platform runtime this attribute must not be set.
""",
- ),
- "implementation_name": attr.string(
- doc = "The Python implementation name (`sys.implementation.name`)",
- default = "cpython",
- ),
- "interpreter": attr.label(
- # We set `allow_files = True` to allow specifying executable
- # targets from rules that have more than one default output,
- # e.g. sh_binary.
- allow_files = True,
- doc = """
+ ),
+ "implementation_name": attr.string(
+ doc = "The Python implementation name (`sys.implementation.name`)",
+ default = "cpython",
+ ),
+ "interpreter": attr.label(
+ # We set `allow_files = True` to allow specifying executable
+ # targets from rules that have more than one default output,
+ # e.g. sh_binary.
+ allow_files = True,
+ doc = """
For an in-build runtime, this is the target to invoke as the interpreter. It
can be either of:
@@ -272,13 +274,13 @@
For a platform runtime (i.e. `interpreter_path` being set) this attribute must
not be set.
""",
- ),
- "interpreter_path": attr.string(doc = """
+ ),
+ "interpreter_path": attr.string(doc = """
For a platform runtime, this is the absolute path of a Python interpreter on
the target platform. For an in-build runtime this attribute must not be set.
"""),
- "interpreter_version_info": attr.string_dict(
- doc = """
+ "interpreter_version_info": attr.string_dict(
+ doc = """
Version information about the interpreter this runtime provides.
If not specified, uses {obj}`--python_version`
@@ -295,20 +297,20 @@
{obj}`--python_version` determines the default value.
:::
""",
- mandatory = False,
- ),
- "pyc_tag": attr.string(
- doc = """
+ mandatory = False,
+ ),
+ "pyc_tag": attr.string(
+ doc = """
Optional string; the tag portion of a pyc filename, e.g. the `cpython-39` infix
of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed
from `implementation_name` and `interpreter_version_info`. If no pyc_tag is
available, then only source-less pyc generation will function correctly.
""",
- ),
- "python_version": attr.string(
- default = "PY3",
- values = ["PY2", "PY3"],
- doc = """
+ ),
+ "python_version": attr.string(
+ default = "PY3",
+ values = ["PY2", "PY3"],
+ doc = """
Whether this runtime is for Python major version 2 or 3. Valid values are `"PY2"`
and `"PY3"`.
@@ -316,32 +318,32 @@
However, in the future this attribute will be mandatory and have no default
value.
""",
- ),
- "site_init_template": attr.label(
- allow_single_file = True,
- default = "//python/private:site_init_template",
- doc = """
+ ),
+ "site_init_template": attr.label(
+ allow_single_file = True,
+ default = "//python/private:site_init_template",
+ doc = """
The template to use for the binary-specific site-init hook run by the
interpreter at startup.
:::{versionadded} 0.41.0
:::
""",
- ),
- "stage2_bootstrap_template": attr.label(
- default = "//python/private:stage2_bootstrap_template",
- allow_single_file = True,
- doc = """
+ ),
+ "stage2_bootstrap_template": attr.label(
+ default = "//python/private:stage2_bootstrap_template",
+ allow_single_file = True,
+ doc = """
The template to use when two stage bootstrapping is enabled
:::{seealso}
{obj}`PyRuntimeInfo.stage2_bootstrap_template` and {obj}`--bootstrap_impl`
:::
""",
- ),
- "stub_shebang": attr.string(
- default = DEFAULT_STUB_SHEBANG,
- doc = """
+ ),
+ "stub_shebang": attr.string(
+ default = DEFAULT_STUB_SHEBANG,
+ doc = """
"Shebang" expression prepended to the bootstrapping Python stub script
used when executing {rule}`py_binary` targets.
@@ -350,11 +352,11 @@
Does not apply to Windows.
""",
- ),
- "zip_main_template": attr.label(
- default = "//python/private:zip_main_template",
- allow_single_file = True,
- doc = """
+ ),
+ "zip_main_template": attr.label(
+ default = "//python/private:zip_main_template",
+ allow_single_file = True,
+ doc = """
The template to use for a zip's top-level `__main__.py` file.
This becomes the entry point executed when `python foo.zip` is run.
@@ -363,14 +365,15 @@
The {obj}`PyRuntimeInfo.zip_main_template` field.
:::
""",
- ),
- "_py_freethreaded_flag": attr.label(
- default = "//python/config_settings:py_freethreaded",
- ),
- "_python_version_flag": attr.label(
- default = "//python/config_settings:python_version",
- ),
- }),
+ ),
+ "_py_freethreaded_flag": attr.label(
+ default = "//python/config_settings:py_freethreaded",
+ ),
+ "_python_version_flag": attr.label(
+ default = "//python/config_settings:python_version",
+ ),
+ },
+ ),
)
def _is_singleton_depset(files):
diff --git a/python/private/py_test_rule.bzl b/python/private/py_test_rule.bzl
index 6ad4fbd..72e8bab 100644
--- a/python/private/py_test_rule.bzl
+++ b/python/private/py_test_rule.bzl
@@ -21,23 +21,6 @@
"py_executable_impl",
)
-_BAZEL_PY_TEST_ATTRS = {
- # This *might* be a magic attribute to help C++ coverage work. There's no
- # docs about this; see TestActionBuilder.java
- "_collect_cc_coverage": attr.label(
- default = "@bazel_tools//tools/test:collect_cc_coverage",
- executable = True,
- cfg = "exec",
- ),
- # This *might* be a magic attribute to help C++ coverage work. There's no
- # docs about this; see TestActionBuilder.java
- "_lcov_merger": attr.label(
- default = configuration_field(fragment = "coverage", name = "output_generator"),
- cfg = "exec",
- executable = True,
- ),
-}
-
def _py_test_impl(ctx):
providers = py_executable_impl(
ctx = ctx,
@@ -53,7 +36,6 @@
test = True,
)
builder.attrs.update(AGNOSTIC_TEST_ATTRS)
- builder.attrs.update(_BAZEL_PY_TEST_ATTRS)
return builder
py_test = create_test_rule_builder().build()
diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl
new file mode 100644
index 0000000..6d9fb3f
--- /dev/null
+++ b/python/private/rule_builders.bzl
@@ -0,0 +1,692 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Builders for creating rules, aspects et al.
+
+When defining rules, Bazel only allows creating *immutable* objects that can't
+be introspected. This makes it difficult to perform arbitrary customizations of
+how a rule is defined, which makes extending a rule implementation prone to
+copy/paste issues and version skew.
+
+These builders are, essentially, mutable and inspectable wrappers for those
+Bazel objects. This allows defining a rule where the values are mutable and
+callers can customize them to derive their own variant of the rule while still
+inheriting everything else about the rule.
+
+To that end, the builders are not strict in how they handle values. They
+generally assume that the values provided are valid and provide ways to
+override their logic and force particular values to be used when they are
+eventually converted to the args for calling e.g. `rule()`.
+
+:::{important}
+When using builders, most lists, dicts, et al passed into them **must** be
+locally created values, otherwise they won't be mutable. This is due to Bazel's
+implicit immutability rules: after evaluating a `.bzl` file, its global
+variables are frozen.
+:::
+
+:::{tip}
+To aid defining reusable pieces, many APIs accept no-arg callable functions
+that create a builder. For example, common attributes can be stored
+in a `dict[str, lambda]`, e.g. `ATTRS = {"srcs": lambda: LabelList(...)}`.
+:::
+
+Example usage:
+
+```
+
+load(":rule_builders.bzl", "ruleb")
+load(":attr_builders.bzl", "attrb")
+
+# File: foo_binary.bzl
+_COMMON_ATTRS = {
+ "srcs": lambda: attrb.LabelList(...),
+}
+
+def create_foo_binary_builder():
+ foo = ruleb.Rule(
+ executable = True,
+ )
+ foo.implementation.set(_foo_binary_impl)
+ foo.attrs.update(COMMON_ATTRS)
+ return foo
+
+def create_foo_test_builder():
+ foo = create_foo_binary_build()
+
+ binary_impl = foo.implementation.get()
+ def foo_test_impl(ctx):
+ binary_impl(ctx)
+ ...
+
+ foo.implementation.set(foo_test_impl)
+ foo.executable.set(False)
+ foo.test.test(True)
+ foo.attrs.update(
+ _coverage = attrb.Label(default="//:coverage")
+ )
+ return foo
+
+foo_binary = create_foo_binary_builder().build()
+foo_test = create_foo_test_builder().build()
+
+# File: custom_foo_binary.bzl
+load(":foo_binary.bzl", "create_foo_binary_builder")
+
+def create_custom_foo_binary():
+ r = create_foo_binary_builder()
+ r.attrs["srcs"].default.append("whatever.txt")
+ return r.build()
+
+custom_foo_binary = create_custom_foo_binary()
+```
+"""
+
+load("@bazel_skylib//lib:types.bzl", "types")
+load(
+ ":builders_util.bzl",
+ "kwargs_getter",
+ "kwargs_getter_doc",
+ "kwargs_set_default_dict",
+ "kwargs_set_default_doc",
+ "kwargs_set_default_ignore_none",
+ "kwargs_set_default_list",
+ "kwargs_setter",
+ "kwargs_setter_doc",
+ "list_add_unique",
+)
+
+# Various string constants for kwarg key names used across two or more
+# functions, or in contexts with optional lookups (e.g. dict.dict, key in dict).
+# Constants are used to reduce the chance of typos.
+# NOTE: These keys are often part of function signature via `**kwargs`; they
+# are not simply internal names.
+_ATTRS = "attrs"
+_CFG = "cfg"
+_EXEC_COMPATIBLE_WITH = "exec_compatible_with"
+_EXEC_GROUPS = "exec_groups"
+_IMPLEMENTATION = "implementation"
+_INPUTS = "inputs"
+_OUTPUTS = "outputs"
+_TOOLCHAINS = "toolchains"
+
+def _is_builder(obj):
+ return hasattr(obj, "build")
+
+def _ExecGroup_typedef():
+ """Builder for {external:bzl:obj}`exec_group`
+
+ :::{function} toolchains() -> list[ToolchainType]
+ :::
+
+ :::{function} exec_compatible_with() -> list[str | Label]
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+ """
+
+def _ExecGroup_new(**kwargs):
+ """Creates a builder for {external:bzl:obj}`exec_group`.
+
+ Args:
+ **kwargs: Same as {external:bzl:obj}`exec_group`
+
+ Returns:
+ {type}`ExecGroup`
+ """
+ kwargs_set_default_list(kwargs, _TOOLCHAINS)
+ kwargs_set_default_list(kwargs, _EXEC_COMPATIBLE_WITH)
+
+ for i, value in enumerate(kwargs[_TOOLCHAINS]):
+ kwargs[_TOOLCHAINS][i] = _ToolchainType_maybe_from(value)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ toolchains = kwargs_getter(kwargs, _TOOLCHAINS),
+ exec_compatible_with = kwargs_getter(kwargs, _EXEC_COMPATIBLE_WITH),
+ kwargs = kwargs,
+ build = lambda: _ExecGroup_build(self),
+ )
+ return self
+
+def _ExecGroup_maybe_from(obj):
+ if types.is_function(obj):
+ return obj()
+ else:
+ return obj
+
+def _ExecGroup_build(self):
+ kwargs = dict(self.kwargs)
+ if kwargs.get(_TOOLCHAINS):
+ kwargs[_TOOLCHAINS] = [
+ v.build() if _is_builder(v) else v
+ for v in kwargs[_TOOLCHAINS]
+ ]
+ if kwargs.get(_EXEC_COMPATIBLE_WITH):
+ kwargs[_EXEC_COMPATIBLE_WITH] = [
+ v.build() if _is_builder(v) else v
+ for v in kwargs[_EXEC_COMPATIBLE_WITH]
+ ]
+ return exec_group(**kwargs)
+
+# buildifier: disable=name-conventions
+ExecGroup = struct(
+ TYPEDEF = _ExecGroup_typedef,
+ new = _ExecGroup_new,
+ build = _ExecGroup_build,
+)
+
+def _ToolchainType_typedef():
+ """Builder for {obj}`config_common.toolchain_type()`
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} mandatory() -> bool
+ :::
+
+ :::{function} name() -> str | Label | None
+ :::
+
+ :::{function} set_name(v: str)
+ :::
+
+ :::{function} set_mandatory(v: bool)
+ :::
+ """
+
+def _ToolchainType_new(name = None, **kwargs):
+ """Creates a builder for `config_common.toolchain_type`.
+
+ Args:
+ name: {type}`str | Label | None` the toolchain type target.
+ **kwargs: Same as {obj}`config_common.toolchain_type`
+
+ Returns:
+ {type}`ToolchainType`
+ """
+ kwargs["name"] = name
+ kwargs_set_default_ignore_none(kwargs, "mandatory", True)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ build = lambda: _ToolchainType_build(self),
+ kwargs = kwargs,
+ mandatory = kwargs_getter(kwargs, "mandatory"),
+ name = kwargs_getter(kwargs, "name"),
+ set_mandatory = kwargs_setter(kwargs, "mandatory"),
+ set_name = kwargs_setter(kwargs, "name"),
+ )
+ return self
+
+def _ToolchainType_maybe_from(obj):
+ if types.is_string(obj) or type(obj) == "Label":
+ return ToolchainType.new(name = obj)
+ elif types.is_function(obj):
+ # A lambda to create a builder
+ return obj()
+ else:
+ # For lack of another option, return it as-is.
+ # Presumably it's already a builder or other valid object.
+ return obj
+
+def _ToolchainType_build(self):
+ """Builds a `config_common.toolchain_type`
+
+ Args:
+ self: implicitly added
+
+ Returns:
+ {type}`config_common.toolchain_type`
+ """
+ kwargs = dict(self.kwargs)
+ name = kwargs.pop("name") # Name must be positional
+ return config_common.toolchain_type(name, **kwargs)
+
+# buildifier: disable=name-conventions
+ToolchainType = struct(
+ TYPEDEF = _ToolchainType_typedef,
+ new = _ToolchainType_new,
+ build = _ToolchainType_build,
+)
+
+def _RuleCfg_typedef():
+ """Wrapper for `rule.cfg` arg.
+
+ :::{function} implementation() -> str | callable | None | config.target | config.none
+ :::
+
+ ::::{function} inputs() -> list[Label]
+
+ :::{seealso}
+ The {obj}`add_inputs()` and {obj}`update_inputs` methods for adding unique
+ values.
+ :::
+ ::::
+
+ :::{function} outputs() -> list[Label]
+
+ :::{seealso}
+ The {obj}`add_outputs()` and {obj}`update_outputs` methods for adding unique
+ values.
+ :::
+ :::
+
+ :::{function} set_implementation(v: str | callable | None | config.target | config.none)
+
+ The string values "target" and "none" are supported.
+ :::
+ """
+
+def _RuleCfg_new(rule_cfg_arg):
+ """Creates a builder for the `rule.cfg` arg.
+
+ Args:
+ rule_cfg_arg: {type}`str | dict | None` The `cfg` arg passed to Rule().
+
+ Returns:
+ {type}`RuleCfg`
+ """
+ state = {}
+ if types.is_dict(rule_cfg_arg):
+ state.update(rule_cfg_arg)
+ else:
+ # Assume its a string, config.target, config.none, or other
+ # valid object.
+ state[_IMPLEMENTATION] = rule_cfg_arg
+
+ kwargs_set_default_list(state, _INPUTS)
+ kwargs_set_default_list(state, _OUTPUTS)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ add_inputs = lambda *a, **k: _RuleCfg_add_inputs(self, *a, **k),
+ add_outputs = lambda *a, **k: _RuleCfg_add_outputs(self, *a, **k),
+ _state = state,
+ build = lambda: _RuleCfg_build(self),
+ implementation = kwargs_getter(state, _IMPLEMENTATION),
+ inputs = kwargs_getter(state, _INPUTS),
+ outputs = kwargs_getter(state, _OUTPUTS),
+ set_implementation = kwargs_setter(state, _IMPLEMENTATION),
+ update_inputs = lambda *a, **k: _RuleCfg_update_inputs(self, *a, **k),
+ update_outputs = lambda *a, **k: _RuleCfg_update_outputs(self, *a, **k),
+ )
+ return self
+
+def _RuleCfg_add_inputs(self, *inputs):
+ """Adds an input to the list of inputs, if not present already.
+
+ :::{seealso}
+ The {obj}`update_inputs()` method for adding a collection of
+ values.
+ :::
+
+ Args:
+ self: implicitly arg.
+ *inputs: {type}`Label` the inputs to add. Note that a `Label`,
+ not `str`, should be passed to ensure different apparent labels
+ can be properly de-duplicated.
+ """
+ self.update_inputs(inputs)
+
+def _RuleCfg_add_outputs(self, *outputs):
+ """Adds an output to the list of outputs, if not present already.
+
+ :::{seealso}
+ The {obj}`update_outputs()` method for adding a collection of
+ values.
+ :::
+
+ Args:
+ self: implicitly arg.
+ *outputs: {type}`Label` the outputs to add. Note that a `Label`,
+ not `str`, should be passed to ensure different apparent labels
+ can be properly de-duplicated.
+ """
+ self.update_outputs(outputs)
+
+def _RuleCfg_build(self):
+ """Builds the rule cfg into the value rule.cfg arg value.
+
+ Returns:
+ {type}`transition` the transition object to apply to the rule.
+ """
+ impl = self._state[_IMPLEMENTATION]
+ if impl == "target" or impl == None:
+ # config.target is Bazel 8+
+ if hasattr(config, "target"):
+ return config.target()
+ else:
+ return None
+ elif impl == "none":
+ return config.none()
+ elif types.is_function(impl):
+ return transition(
+ implementation = impl,
+ # Transitions only accept unique lists of strings.
+ inputs = {str(v): None for v in self._state[_INPUTS]}.keys(),
+ outputs = {str(v): None for v in self._state[_OUTPUTS]}.keys(),
+ )
+ else:
+ # Assume its valid. Probably an `config.XXX` object or manually
+ # set transition object.
+ return impl
+
+def _RuleCfg_update_inputs(self, *others):
+ """Add a collection of values to inputs.
+
+ Args:
+ self: implicitly added
+ *others: {type}`collection[Label]` collection of labels to add to
+ inputs. Only values not already present are added. Note that a
+ `Label`, not `str`, should be passed to ensure different apparent
+ labels can be properly de-duplicated.
+ """
+ list_add_unique(self._state[_INPUTS], others)
+
+def _RuleCfg_update_outputs(self, *others):
+ """Add a collection of values to outputs.
+
+ Args:
+ self: implicitly added
+ *others: {type}`collection[Label]` collection of labels to add to
+ outputs. Only values not already present are added. Note that a
+ `Label`, not `str`, should be passed to ensure different apparent
+ labels can be properly de-duplicated.
+ """
+ list_add_unique(self._state[_OUTPUTS], others)
+
+# buildifier: disable=name-conventions
+RuleCfg = struct(
+ TYPEDEF = _RuleCfg_typedef,
+ new = _RuleCfg_new,
+ # keep sorted
+ add_inputs = _RuleCfg_add_inputs,
+ add_outputs = _RuleCfg_add_outputs,
+ build = _RuleCfg_build,
+ update_inputs = _RuleCfg_update_inputs,
+ update_outputs = _RuleCfg_update_outputs,
+)
+
+def _Rule_typedef():
+ """A builder to accumulate state for constructing a `rule` object.
+
+ :::{field} attrs
+ :type: AttrsDict
+ :::
+
+ :::{field} cfg
+ :type: RuleCfg
+ :::
+
+ :::{function} doc() -> str
+ :::
+
+ :::{function} exec_groups() -> dict[str, ExecGroup]
+ :::
+
+ :::{function} executable() -> bool
+ :::
+
+ :::{include} /_includes/field_kwargs_doc.md
+ :::
+
+ :::{function} fragments() -> list[str]
+ :::
+
+ :::{function} implementation() -> callable | None
+ :::
+
+ :::{function} provides() -> list[provider | list[provider]]
+ :::
+
+ :::{function} set_doc(v: str)
+ :::
+
+ :::{function} set_executable(v: bool)
+ :::
+
+ :::{function} set_implementation(v: callable)
+ :::
+
+ :::{function} set_test(v: bool)
+ :::
+
+ :::{function} test() -> bool
+ :::
+
+ :::{function} toolchains() -> list[ToolchainType]
+ :::
+ """
+
+def _Rule_new(**kwargs):
+ """Builder for creating rules.
+
+ Args:
+ **kwargs: The same as the `rule()` function, but using builders or
+ dicts to specify sub-objects instead of the immutable Bazel
+ objects.
+ """
+ kwargs.setdefault(_IMPLEMENTATION, None)
+ kwargs_set_default_doc(kwargs)
+ kwargs_set_default_dict(kwargs, _EXEC_GROUPS)
+ kwargs_set_default_ignore_none(kwargs, "executable", False)
+ kwargs_set_default_list(kwargs, "fragments")
+ kwargs_set_default_list(kwargs, "provides")
+ kwargs_set_default_ignore_none(kwargs, "test", False)
+ kwargs_set_default_list(kwargs, _TOOLCHAINS)
+
+ for name, value in kwargs[_EXEC_GROUPS].items():
+ kwargs[_EXEC_GROUPS][name] = _ExecGroup_maybe_from(value)
+
+ for i, value in enumerate(kwargs[_TOOLCHAINS]):
+ kwargs[_TOOLCHAINS][i] = _ToolchainType_maybe_from(value)
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ attrs = _AttrsDict_new(kwargs.pop(_ATTRS, None)),
+ build = lambda *a, **k: _Rule_build(self, *a, **k),
+ cfg = _RuleCfg_new(kwargs.pop(_CFG, None)),
+ doc = kwargs_getter_doc(kwargs),
+ exec_groups = kwargs_getter(kwargs, _EXEC_GROUPS),
+ executable = kwargs_getter(kwargs, "executable"),
+ fragments = kwargs_getter(kwargs, "fragments"),
+ implementation = kwargs_getter(kwargs, _IMPLEMENTATION),
+ kwargs = kwargs,
+ provides = kwargs_getter(kwargs, "provides"),
+ set_doc = kwargs_setter_doc(kwargs),
+ set_executable = kwargs_setter(kwargs, "executable"),
+ set_implementation = kwargs_setter(kwargs, _IMPLEMENTATION),
+ set_test = kwargs_setter(kwargs, "test"),
+ test = kwargs_getter(kwargs, "test"),
+ to_kwargs = lambda: _Rule_to_kwargs(self),
+ toolchains = kwargs_getter(kwargs, _TOOLCHAINS),
+ )
+ return self
+
+def _Rule_build(self, debug = ""):
+ """Builds a `rule` object
+
+ Args:
+ self: implicitly added
+ debug: {type}`str` If set, prints the args used to create the rule.
+
+ Returns:
+ {type}`rule`
+ """
+ kwargs = self.to_kwargs()
+ if debug:
+ lines = ["=" * 80, "rule kwargs: {}:".format(debug)]
+ for k, v in sorted(kwargs.items()):
+ if types.is_dict(v):
+ lines.append(" %s={" % k)
+ for k2, v2 in sorted(v.items()):
+ lines.append(" {}: {}".format(k2, v2))
+ lines.append(" }")
+ elif types.is_list(v):
+ lines.append(" {}=[".format(k))
+ for i, v2 in enumerate(v):
+ lines.append(" [{}] {}".format(i, v2))
+ lines.append(" ]")
+ else:
+ lines.append(" {}={}".format(k, v))
+ print("\n".join(lines)) # buildifier: disable=print
+ return rule(**kwargs)
+
+def _Rule_to_kwargs(self):
+ """Builds the arguments for calling `rule()`.
+
+ This is added as an escape hatch to construct the final values `rule()`
+ kwarg values in case callers want to manually change them.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ {type}`dict`
+ """
+ kwargs = dict(self.kwargs)
+ if _EXEC_GROUPS in kwargs:
+ kwargs[_EXEC_GROUPS] = {
+ k: v.build() if _is_builder(v) else v
+ for k, v in kwargs[_EXEC_GROUPS].items()
+ }
+ if _TOOLCHAINS in kwargs:
+ kwargs[_TOOLCHAINS] = [
+ v.build() if _is_builder(v) else v
+ for v in kwargs[_TOOLCHAINS]
+ ]
+ if _ATTRS not in kwargs:
+ kwargs[_ATTRS] = self.attrs.build()
+ if _CFG not in kwargs:
+ kwargs[_CFG] = self.cfg.build()
+ return kwargs
+
+# buildifier: disable=name-conventions
+Rule = struct(
+ TYPEDEF = _Rule_typedef,
+ new = _Rule_new,
+ build = _Rule_build,
+ to_kwargs = _Rule_to_kwargs,
+)
+
+def _AttrsDict_typedef():
+ """Builder for the dictionary of rule attributes.
+
+ :::{field} map
+ :type: dict[str, AttributeBuilder]
+
+ The underlying dict of attributes. Directly accessible so that regular
+ dict operations (e.g. `x in y`) can be performed, if necessary.
+ :::
+
+ :::{function} get(key, default=None)
+ Get an entry from the dict. Convenience wrapper for `.map.get(...)`
+ :::
+
+ :::{function} items() -> list[tuple[str, object]]
+ Returns a list of key-value tuples. Convenience wrapper for `.map.items()`
+ :::
+
+ :::{function} pop(key, default) -> object
+ Removes a key from the attr dict
+ :::
+ """
+
+def _AttrsDict_new(initial):
+ """Creates a builder for the `rule.attrs` dict.
+
+ Args:
+ initial: {type}`dict[str, callable | AttributeBuilder] | None` dict of
+ initial values to populate the attributes dict with.
+
+ Returns:
+ {type}`AttrsDict`
+ """
+
+ # buildifier: disable=uninitialized
+ self = struct(
+ # keep sorted
+ build = lambda: _AttrsDict_build(self),
+ get = lambda *a, **k: self.map.get(*a, **k),
+ items = lambda: self.map.items(),
+ map = {},
+ put = lambda key, value: _AttrsDict_put(self, key, value),
+ update = lambda *a, **k: _AttrsDict_update(self, *a, **k),
+ pop = lambda *a, **k: self.map.pop(*a, **k),
+ )
+ if initial:
+ _AttrsDict_update(self, initial)
+ return self
+
+def _AttrsDict_put(self, name, value):
+ """Sets a value in the attrs dict.
+
+ Args:
+ self: implicitly added
+ name: {type}`str` the attribute name to set in the dict
+ value: {type}`AttributeBuilder | callable` the value for the
+ attribute. If a callable, then it is treated as an
+ attribute builder factory (no-arg callable that returns an
+ attribute builder) and is called immediately.
+ """
+ if types.is_function(value):
+ # Convert factory function to builder
+ value = value()
+ self.map[name] = value
+
+def _AttrsDict_update(self, other):
+ """Merge `other` into this object.
+
+ Args:
+ self: implicitly added
+ other: {type}`dict[str, callable | AttributeBuilder]` the values to
+ merge into this object. If the value a function, it is called
+ with no args and expected to return an attribute builder. This
+ allows defining dicts of common attributes (where the values are
+ functions that create a builder) and merge them into the rule.
+ """
+ for k, v in other.items():
+ # Handle factory functions that create builders
+ if types.is_function(v):
+ self.map[k] = v()
+ else:
+ self.map[k] = v
+
+def _AttrsDict_build(self):
+ """Build an attribute dict for passing to `rule()`.
+
+ Returns:
+ {type}`dict[str, attribute]` where the values are `attr.XXX` objects
+ """
+ attrs = {}
+ for k, v in self.map.items():
+ attrs[k] = v.build() if _is_builder(v) else v
+ return attrs
+
+# buildifier: disable=name-conventions
+AttrsDict = struct(
+ TYPEDEF = _AttrsDict_typedef,
+ new = _AttrsDict_new,
+ update = _AttrsDict_update,
+ build = _AttrsDict_build,
+)
+
+ruleb = struct(
+ Rule = _Rule_new,
+ ToolchainType = _ToolchainType_new,
+ ExecGroup = _ExecGroup_new,
+)
diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt
index 969c772..dc11f02 100644
--- a/sphinxdocs/inventories/bazel_inventory.txt
+++ b/sphinxdocs/inventories/bazel_inventory.txt
@@ -15,10 +15,17 @@
ToolchainInfo bzl:type 1 rules/lib/providers/ToolchainInfo.html -
attr.bool bzl:type 1 rules/lib/toplevel/attr#bool -
attr.int bzl:type 1 rules/lib/toplevel/attr#int -
+attr.int_list bzl:type 1 rules/lib/toplevel/attr#int_list -
attr.label bzl:type 1 rules/lib/toplevel/attr#label -
+attr.label_keyed_string_dict bzl:type 1 rules/lib/toplevel/attr#label_keyed_string_dict -
attr.label_list bzl:type 1 rules/lib/toplevel/attr#label_list -
+attr.output bzl:type 1 rules/lib/toplevel/attr#output -
+attr.output_list bzl:type 1 rules/lib/toplevel/attr#output_list -
attr.string bzl:type 1 rules/lib/toplevel/attr#string -
+attr.string_dict bzl:type 1 rules/lib/toplevel/attr#string_dict -
+attr.string_keyed_label_dict bzl:type 1 rules/lib/toplevel/attr#string_keyed_label_dict -
attr.string_list bzl:type 1 rules/lib/toplevel/attr#string_list -
+attr.string_list_dict bzl:type 1 rules/lib/toplevel/attr#string_list_dict -
bool bzl:type 1 rules/lib/bool -
callable bzl:type 1 rules/lib/core/function -
config_common.FeatureFlagInfo bzl:type 1 rules/lib/toplevel/config_common#FeatureFlagInfo -
@@ -60,6 +67,7 @@
depset bzl:type 1 rules/lib/depset -
dict bzl:type 1 rules/lib/dict -
exec_compatible_with bzl:attr 1 reference/be/common-definitions#common.exec_compatible_with -
+exec_group bzl:function 1 rules/lib/globals/bzl#exec_group -
int bzl:type 1 rules/lib/int -
label bzl:type 1 concepts/labels -
list bzl:type 1 rules/lib/list -
diff --git a/tests/builders/BUILD.bazel b/tests/builders/BUILD.bazel
index 3ad0c3e..f963cb0 100644
--- a/tests/builders/BUILD.bazel
+++ b/tests/builders/BUILD.bazel
@@ -12,6 +12,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+load(":attr_builders_tests.bzl", "attr_builders_test_suite")
load(":builders_tests.bzl", "builders_test_suite")
+load(":rule_builders_tests.bzl", "rule_builders_test_suite")
builders_test_suite(name = "builders_test_suite")
+
+rule_builders_test_suite(name = "rule_builders_test_suite")
+
+attr_builders_test_suite(name = "attr_builders_test_suite")
+
+toolchain_type(name = "tct_1")
+
+toolchain_type(name = "tct_2")
+
+toolchain_type(name = "tct_3")
+
+toolchain_type(name = "tct_4")
+
+toolchain_type(name = "tct_5")
+
+filegroup(name = "empty")
+
+toolchain(
+ name = "tct_3_toolchain",
+ toolchain = "//tests/support/empty_toolchain:empty",
+ toolchain_type = "//tests/builders:tct_3",
+)
+
+toolchain(
+ name = "tct_4_toolchain",
+ toolchain = "//tests/support/empty_toolchain:empty",
+ toolchain_type = ":tct_4",
+)
+
+toolchain(
+ name = "tct_5_toolchain",
+ toolchain = "//tests/support/empty_toolchain:empty",
+ toolchain_type = ":tct_5",
+)
diff --git a/tests/builders/attr_builders_tests.bzl b/tests/builders/attr_builders_tests.bzl
new file mode 100644
index 0000000..58557cd
--- /dev/null
+++ b/tests/builders/attr_builders_tests.bzl
@@ -0,0 +1,468 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for attr_builders."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:truth.bzl", "truth")
+load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility
+
+def _expect_cfg_defaults(expect, cfg):
+ expect.where(expr = "cfg.outputs").that_collection(cfg.outputs()).contains_exactly([])
+ expect.where(expr = "cfg.inputs").that_collection(cfg.inputs()).contains_exactly([])
+ expect.where(expr = "cfg.implementation").that_bool(cfg.implementation()).equals(None)
+ expect.where(expr = "cfg.target").that_bool(cfg.target()).equals(True)
+ expect.where(expr = "cfg.exec_group").that_str(cfg.exec_group()).equals(None)
+ expect.where(expr = "cfg.which_cfg").that_str(cfg.which_cfg()).equals("target")
+
+_some_aspect = aspect(implementation = lambda target, ctx: None)
+
+_tests = []
+
+def _report_failures(name, env):
+ failures = env.failures
+
+ def _report_failures_impl(env, target):
+ _ = target # @unused
+ env._failures.extend(failures)
+
+ analysis_test(
+ name = name,
+ target = "//python:none",
+ impl = _report_failures_impl,
+ )
+
+# Calling attr.xxx() outside of the loading phase is an error, but rules_testing
+# creates the expect/truth helpers during the analysis phase. To make the truth
+# helpers available during the loading phase, fake out the ctx just enough to
+# satify rules_testing.
+def _loading_phase_expect(test_name):
+ env = struct(
+ ctx = struct(
+ workspace_name = "bogus",
+ label = Label(test_name),
+ attr = struct(
+ _impl_name = test_name,
+ ),
+ ),
+ failures = [],
+ )
+ return env, truth.expect(env)
+
+def _expect_builds(expect, builder, attribute_type):
+ expect.that_str(str(builder.build())).contains(attribute_type)
+
+def _test_cfg_arg(name):
+ env, _ = _loading_phase_expect(name)
+
+ def build_cfg(cfg):
+ attrb.Label(cfg = cfg).build()
+
+ build_cfg(None)
+ build_cfg("target")
+ build_cfg("exec")
+ build_cfg(dict(exec_group = "eg"))
+ build_cfg(dict(implementation = (lambda settings, attr: None)))
+ build_cfg(config.exec())
+ build_cfg(transition(
+ implementation = (lambda settings, attr: None),
+ inputs = [],
+ outputs = [],
+ ))
+
+ # config.target is Bazel 8+
+ if hasattr(config, "target"):
+ build_cfg(config.target())
+
+ # config.none is Bazel 8+
+ if hasattr(config, "none"):
+ build_cfg("none")
+ build_cfg(config.none())
+
+ _report_failures(name, env)
+
+_tests.append(_test_cfg_arg)
+
+def _test_bool(name):
+ env, expect = _loading_phase_expect(name)
+ subject = attrb.Bool()
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.default()).equals(False)
+ expect.that_bool(subject.mandatory()).equals(False)
+ _expect_builds(expect, subject, "attr.bool")
+
+ subject.set_default(True)
+ subject.set_mandatory(True)
+ subject.set_doc("doc")
+
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.default()).equals(True)
+ expect.that_bool(subject.mandatory()).equals(True)
+ _expect_builds(expect, subject, "attr.bool")
+
+ _report_failures(name, env)
+
+_tests.append(_test_bool)
+
+def _test_int(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.Int()
+ expect.that_int(subject.default()).equals(0)
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_collection(subject.values()).contains_exactly([])
+ _expect_builds(expect, subject, "attr.int")
+
+ subject.set_default(42)
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.values().append(42)
+
+ expect.that_int(subject.default()).equals(42)
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_collection(subject.values()).contains_exactly([42])
+ _expect_builds(expect, subject, "attr.int")
+
+ _report_failures(name, env)
+
+_tests.append(_test_int)
+
+def _test_int_list(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.IntList()
+ expect.that_bool(subject.allow_empty()).equals(True)
+ expect.that_collection(subject.default()).contains_exactly([])
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ _expect_builds(expect, subject, "attr.int_list")
+
+ subject.default().append(99)
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+
+ expect.that_collection(subject.default()).contains_exactly([99])
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ _expect_builds(expect, subject, "attr.int_list")
+
+ _report_failures(name, env)
+
+_tests.append(_test_int_list)
+
+def _test_label(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.Label()
+
+ expect.that_str(subject.default()).equals(None)
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.executable()).equals(False)
+ expect.that_bool(subject.allow_files()).equals(None)
+ expect.that_bool(subject.allow_single_file()).equals(None)
+ expect.that_collection(subject.providers()).contains_exactly([])
+ expect.that_collection(subject.aspects()).contains_exactly([])
+ _expect_cfg_defaults(expect, subject.cfg)
+ _expect_builds(expect, subject, "attr.label")
+
+ subject.set_default("//foo:bar")
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.set_executable(True)
+ subject.add_allow_files(".txt")
+ subject.cfg.set_target()
+ subject.providers().append("provider")
+ subject.aspects().append(_some_aspect)
+ subject.cfg.outputs().append(Label("//some:output"))
+ subject.cfg.inputs().append(Label("//some:input"))
+ impl = lambda: None
+ subject.cfg.set_implementation(impl)
+
+ expect.that_str(subject.default()).equals("//foo:bar")
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_bool(subject.executable()).equals(True)
+ expect.that_collection(subject.allow_files()).contains_exactly([".txt"])
+ expect.that_bool(subject.allow_single_file()).equals(None)
+ expect.that_collection(subject.providers()).contains_exactly(["provider"])
+ expect.that_collection(subject.aspects()).contains_exactly([_some_aspect])
+ expect.that_collection(subject.cfg.outputs()).contains_exactly([Label("//some:output")])
+ expect.that_collection(subject.cfg.inputs()).contains_exactly([Label("//some:input")])
+ expect.that_bool(subject.cfg.implementation()).equals(impl)
+ _expect_builds(expect, subject, "attr.label")
+
+ _report_failures(name, env)
+
+_tests.append(_test_label)
+
+def _test_label_keyed_string_dict(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.LabelKeyedStringDict()
+
+ expect.that_dict(subject.default()).contains_exactly({})
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_files()).equals(False)
+ expect.that_collection(subject.providers()).contains_exactly([])
+ expect.that_collection(subject.aspects()).contains_exactly([])
+ _expect_cfg_defaults(expect, subject.cfg)
+ _expect_builds(expect, subject, "attr.label_keyed_string_dict")
+
+ subject.default()["key"] = "//some:label"
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.set_allow_files(True)
+ subject.cfg.set_target()
+ subject.providers().append("provider")
+ subject.aspects().append(_some_aspect)
+ subject.cfg.outputs().append("//some:output")
+ subject.cfg.inputs().append("//some:input")
+ impl = lambda: None
+ subject.cfg.set_implementation(impl)
+
+ expect.that_dict(subject.default()).contains_exactly({"key": "//some:label"})
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_bool(subject.allow_files()).equals(True)
+ expect.that_collection(subject.providers()).contains_exactly(["provider"])
+ expect.that_collection(subject.aspects()).contains_exactly([_some_aspect])
+ expect.that_collection(subject.cfg.outputs()).contains_exactly(["//some:output"])
+ expect.that_collection(subject.cfg.inputs()).contains_exactly(["//some:input"])
+ expect.that_bool(subject.cfg.implementation()).equals(impl)
+
+ _expect_builds(expect, subject, "attr.label_keyed_string_dict")
+
+ subject.add_allow_files(".txt")
+ expect.that_collection(subject.allow_files()).contains_exactly([".txt"])
+ _expect_builds(expect, subject, "attr.label_keyed_string_dict")
+
+ _report_failures(name, env)
+
+_tests.append(_test_label_keyed_string_dict)
+
+def _test_label_list(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.LabelList()
+
+ expect.that_collection(subject.default()).contains_exactly([])
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_files()).equals(False)
+ expect.that_collection(subject.providers()).contains_exactly([])
+ expect.that_collection(subject.aspects()).contains_exactly([])
+ _expect_cfg_defaults(expect, subject.cfg)
+ _expect_builds(expect, subject, "attr.label_list")
+
+ subject.default().append("//some:label")
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.set_allow_files([".txt"])
+ subject.providers().append("provider")
+ subject.aspects().append(_some_aspect)
+
+ expect.that_collection(subject.default()).contains_exactly(["//some:label"])
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_collection(subject.allow_files()).contains_exactly([".txt"])
+ expect.that_collection(subject.providers()).contains_exactly(["provider"])
+ expect.that_collection(subject.aspects()).contains_exactly([_some_aspect])
+
+ _expect_builds(expect, subject, "attr.label_list")
+
+ _report_failures(name, env)
+
+_tests.append(_test_label_list)
+
+def _test_output(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.Output()
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ _expect_builds(expect, subject, "attr.output")
+
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ _expect_builds(expect, subject, "attr.output")
+
+ _report_failures(name, env)
+
+_tests.append(_test_output)
+
+def _test_output_list(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.OutputList()
+ expect.that_bool(subject.allow_empty()).equals(True)
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ _expect_builds(expect, subject, "attr.output_list")
+
+ subject.set_allow_empty(False)
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ expect.that_bool(subject.allow_empty()).equals(False)
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ _expect_builds(expect, subject, "attr.output_list")
+
+ _report_failures(name, env)
+
+_tests.append(_test_output_list)
+
+def _test_string(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.String()
+ expect.that_str(subject.default()).equals("")
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_collection(subject.values()).contains_exactly([])
+ _expect_builds(expect, subject, "attr.string")
+
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.values().append("green")
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_collection(subject.values()).contains_exactly(["green"])
+ _expect_builds(expect, subject, "attr.string")
+
+ _report_failures(name, env)
+
+_tests.append(_test_string)
+
+def _test_string_dict(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.StringDict()
+
+ expect.that_dict(subject.default()).contains_exactly({})
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_empty()).equals(True)
+ _expect_builds(expect, subject, "attr.string_dict")
+
+ subject.default()["key"] = "value"
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.set_allow_empty(False)
+
+ expect.that_dict(subject.default()).contains_exactly({"key": "value"})
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_bool(subject.allow_empty()).equals(False)
+ _expect_builds(expect, subject, "attr.string_dict")
+
+ _report_failures(name, env)
+
+_tests.append(_test_string_dict)
+
+def _test_string_keyed_label_dict(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.StringKeyedLabelDict()
+
+ expect.that_dict(subject.default()).contains_exactly({})
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_files()).equals(False)
+ expect.that_collection(subject.providers()).contains_exactly([])
+ expect.that_collection(subject.aspects()).contains_exactly([])
+ _expect_cfg_defaults(expect, subject.cfg)
+ _expect_builds(expect, subject, "attr.string_keyed_label_dict")
+
+ subject.default()["key"] = "//some:label"
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.set_allow_files([".txt"])
+ subject.providers().append("provider")
+ subject.aspects().append(_some_aspect)
+
+ expect.that_dict(subject.default()).contains_exactly({"key": "//some:label"})
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_collection(subject.allow_files()).contains_exactly([".txt"])
+ expect.that_collection(subject.providers()).contains_exactly(["provider"])
+ expect.that_collection(subject.aspects()).contains_exactly([_some_aspect])
+
+ _expect_builds(expect, subject, "attr.string_keyed_label_dict")
+
+ _report_failures(name, env)
+
+_tests.append(_test_string_keyed_label_dict)
+
+def _test_string_list(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.StringList()
+
+ expect.that_collection(subject.default()).contains_exactly([])
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_empty()).equals(True)
+ _expect_builds(expect, subject, "attr.string_list")
+
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.default().append("blue")
+ subject.set_allow_empty(False)
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_bool(subject.allow_empty()).equals(False)
+ expect.that_collection(subject.default()).contains_exactly(["blue"])
+ _expect_builds(expect, subject, "attr.string_list")
+
+ _report_failures(name, env)
+
+_tests.append(_test_string_list)
+
+def _test_string_list_dict(name):
+ env, expect = _loading_phase_expect(name)
+
+ subject = attrb.StringListDict()
+
+ expect.that_dict(subject.default()).contains_exactly({})
+ expect.that_str(subject.doc()).equals("")
+ expect.that_bool(subject.mandatory()).equals(False)
+ expect.that_bool(subject.allow_empty()).equals(True)
+ _expect_builds(expect, subject, "attr.string_list_dict")
+
+ subject.set_doc("doc")
+ subject.set_mandatory(True)
+ subject.default()["key"] = ["red"]
+ subject.set_allow_empty(False)
+ expect.that_str(subject.doc()).equals("doc")
+ expect.that_bool(subject.mandatory()).equals(True)
+ expect.that_bool(subject.allow_empty()).equals(False)
+ expect.that_dict(subject.default()).contains_exactly({"key": ["red"]})
+ _expect_builds(expect, subject, "attr.string_list_dict")
+
+ _report_failures(name, env)
+
+_tests.append(_test_string_list_dict)
+
+def attr_builders_test_suite(name):
+ test_suite(
+ name = name,
+ tests = _tests,
+ )
diff --git a/tests/builders/rule_builders_tests.bzl b/tests/builders/rule_builders_tests.bzl
new file mode 100644
index 0000000..9a91ceb
--- /dev/null
+++ b/tests/builders/rule_builders_tests.bzl
@@ -0,0 +1,256 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for rule_builders."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:util.bzl", "TestingAspectInfo")
+load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility
+load("//python/private:rule_builders.bzl", "ruleb") # buildifier: disable=bzl-visibility
+
+RuleInfo = provider(doc = "test provider", fields = [])
+
+_tests = [] # analysis-phase tests
+_basic_tests = [] # loading-phase tests
+
+fruit = ruleb.Rule(
+ implementation = lambda ctx: [RuleInfo()],
+ attrs = {
+ "color": attrb.String(default = "yellow"),
+ "fertilizers": attrb.LabelList(
+ allow_files = True,
+ ),
+ "flavors": attrb.StringList(),
+ "nope": attr.label(
+ # config.none is Bazel 8+
+ cfg = config.none() if hasattr(config, "none") else None,
+ ),
+ "organic": lambda: attrb.Bool(),
+ "origin": lambda: attrb.Label(),
+ "size": lambda: attrb.Int(default = 10),
+ },
+).build()
+
+def _test_fruit_rule(name):
+ fruit(
+ name = name + "_subject",
+ flavors = ["spicy", "sweet"],
+ organic = True,
+ size = 5,
+ origin = "//python:none",
+ fertilizers = [
+ "nitrogen.txt",
+ "phosphorus.txt",
+ ],
+ )
+
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_fruit_rule_impl,
+ )
+
+def _test_fruit_rule_impl(env, target):
+ attrs = target[TestingAspectInfo].attrs
+ env.expect.that_str(attrs.color).equals("yellow")
+ env.expect.that_collection(attrs.flavors).contains_exactly(["spicy", "sweet"])
+ env.expect.that_bool(attrs.organic).equals(True)
+ env.expect.that_int(attrs.size).equals(5)
+
+ # //python:none is an alias to //python/private:sentinel; we see the
+ # resolved value, not the intermediate alias
+ env.expect.that_target(attrs.origin).label().equals(Label("//python/private:sentinel"))
+
+ env.expect.that_collection(attrs.fertilizers).transform(
+ desc = "target.label",
+ map_each = lambda t: t.label,
+ ).contains_exactly([
+ Label(":nitrogen.txt"),
+ Label(":phosphorus.txt"),
+ ])
+
+_tests.append(_test_fruit_rule)
+
+# NOTE: `Rule.build()` can't be called because it's not during the top-level
+# bzl evaluation.
+def _test_rule_api(env):
+ subject = ruleb.Rule()
+ expect = env.expect
+
+ expect.that_dict(subject.attrs.map).contains_exactly({})
+ expect.that_collection(subject.cfg.outputs()).contains_exactly([])
+ expect.that_collection(subject.cfg.inputs()).contains_exactly([])
+ expect.that_bool(subject.cfg.implementation()).equals(None)
+ expect.that_str(subject.doc()).equals("")
+ expect.that_dict(subject.exec_groups()).contains_exactly({})
+ expect.that_bool(subject.executable()).equals(False)
+ expect.that_collection(subject.fragments()).contains_exactly([])
+ expect.that_bool(subject.implementation()).equals(None)
+ expect.that_collection(subject.provides()).contains_exactly([])
+ expect.that_bool(subject.test()).equals(False)
+ expect.that_collection(subject.toolchains()).contains_exactly([])
+
+ subject.attrs.update({
+ "builder": attrb.String(),
+ "factory": lambda: attrb.String(),
+ })
+ subject.attrs.put("put_factory", lambda: attrb.Int())
+ subject.attrs.put("put_builder", attrb.Int())
+
+ expect.that_dict(subject.attrs.map).keys().contains_exactly([
+ "factory",
+ "builder",
+ "put_factory",
+ "put_builder",
+ ])
+ expect.that_collection(subject.attrs.map.values()).transform(
+ desc = "type() of attr value",
+ map_each = type,
+ ).contains_exactly(["struct", "struct", "struct", "struct"])
+
+ subject.set_doc("doc")
+ expect.that_str(subject.doc()).equals("doc")
+
+ subject.exec_groups()["eg"] = ruleb.ExecGroup()
+ expect.that_dict(subject.exec_groups()).keys().contains_exactly(["eg"])
+
+ subject.set_executable(True)
+ expect.that_bool(subject.executable()).equals(True)
+
+ subject.fragments().append("frag")
+ expect.that_collection(subject.fragments()).contains_exactly(["frag"])
+
+ impl = lambda: None
+ subject.set_implementation(impl)
+ expect.that_bool(subject.implementation()).equals(impl)
+
+ subject.provides().append(RuleInfo)
+ expect.that_collection(subject.provides()).contains_exactly([RuleInfo])
+
+ subject.set_test(True)
+ expect.that_bool(subject.test()).equals(True)
+
+ subject.toolchains().append(ruleb.ToolchainType())
+ expect.that_collection(subject.toolchains()).has_size(1)
+
+ expect.that_collection(subject.cfg.outputs()).contains_exactly([])
+ expect.that_collection(subject.cfg.inputs()).contains_exactly([])
+ expect.that_bool(subject.cfg.implementation()).equals(None)
+
+ subject.cfg.set_implementation(impl)
+ expect.that_bool(subject.cfg.implementation()).equals(impl)
+ subject.cfg.add_inputs(Label("//some:input"))
+ expect.that_collection(subject.cfg.inputs()).contains_exactly([
+ Label("//some:input"),
+ ])
+ subject.cfg.add_outputs(Label("//some:output"))
+ expect.that_collection(subject.cfg.outputs()).contains_exactly([
+ Label("//some:output"),
+ ])
+
+_basic_tests.append(_test_rule_api)
+
+def _test_exec_group(env):
+ subject = ruleb.ExecGroup()
+
+ env.expect.that_collection(subject.toolchains()).contains_exactly([])
+ env.expect.that_collection(subject.exec_compatible_with()).contains_exactly([])
+ env.expect.that_str(str(subject.build())).contains("ExecGroup")
+
+ subject.toolchains().append(ruleb.ToolchainType("//python:none"))
+ subject.exec_compatible_with().append("//some:constraint")
+ env.expect.that_str(str(subject.build())).contains("ExecGroup")
+
+_basic_tests.append(_test_exec_group)
+
+def _test_toolchain_type(env):
+ subject = ruleb.ToolchainType()
+
+ env.expect.that_str(subject.name()).equals(None)
+ env.expect.that_bool(subject.mandatory()).equals(True)
+ subject.set_name("//some:toolchain_type")
+ env.expect.that_str(str(subject.build())).contains("ToolchainType")
+
+ subject.set_name("//some:toolchain_type")
+ subject.set_mandatory(False)
+ env.expect.that_str(subject.name()).equals("//some:toolchain_type")
+ env.expect.that_bool(subject.mandatory()).equals(False)
+ env.expect.that_str(str(subject.build())).contains("ToolchainType")
+
+_basic_tests.append(_test_toolchain_type)
+
+rule_with_toolchains = ruleb.Rule(
+ implementation = lambda ctx: [],
+ toolchains = [
+ ruleb.ToolchainType("//tests/builders:tct_1", mandatory = False),
+ lambda: ruleb.ToolchainType("//tests/builders:tct_2", mandatory = False),
+ "//tests/builders:tct_3",
+ Label("//tests/builders:tct_4"),
+ ],
+ exec_groups = {
+ "eg1": ruleb.ExecGroup(
+ toolchains = [
+ ruleb.ToolchainType("//tests/builders:tct_1", mandatory = False),
+ lambda: ruleb.ToolchainType("//tests/builders:tct_2", mandatory = False),
+ "//tests/builders:tct_3",
+ Label("//tests/builders:tct_4"),
+ ],
+ ),
+ "eg2": lambda: ruleb.ExecGroup(),
+ },
+).build()
+
+def _test_rule_with_toolchains(name):
+ rule_with_toolchains(
+ name = name + "_subject",
+ tags = ["manual"], # Can't be built without extra_toolchains set
+ )
+
+ analysis_test(
+ name = name,
+ impl = lambda env, target: None,
+ target = name + "_subject",
+ config_settings = {
+ "//command_line_option:extra_toolchains": [
+ Label("//tests/builders:all"),
+ ],
+ },
+ )
+
+_tests.append(_test_rule_with_toolchains)
+
+rule_with_immutable_attrs = ruleb.Rule(
+ implementation = lambda ctx: [],
+ attrs = {
+ "foo": attr.string(),
+ },
+).build()
+
+def _test_rule_with_immutable_attrs(name):
+ rule_with_immutable_attrs(name = name + "_subject")
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = lambda env, target: None,
+ )
+
+_tests.append(_test_rule_with_immutable_attrs)
+
+def rule_builders_test_suite(name):
+ test_suite(
+ name = name,
+ basic_tests = _basic_tests,
+ tests = _tests,
+ )
diff --git a/tests/support/empty_toolchain/BUILD.bazel b/tests/support/empty_toolchain/BUILD.bazel
new file mode 100644
index 0000000..cab5f80
--- /dev/null
+++ b/tests/support/empty_toolchain/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":empty.bzl", "empty_toolchain")
+
+empty_toolchain(name = "empty")
diff --git a/tests/support/empty_toolchain/empty.bzl b/tests/support/empty_toolchain/empty.bzl
new file mode 100644
index 0000000..e283928
--- /dev/null
+++ b/tests/support/empty_toolchain/empty.bzl
@@ -0,0 +1,23 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Defines an empty toolchain that returns just ToolchainInfo."""
+
+def _empty_toolchain_impl(ctx):
+ # Include the label so e.g. tests can identify what the target was.
+ return [platform_common.ToolchainInfo(label = ctx.label)]
+
+empty_toolchain = rule(
+ implementation = _empty_toolchain_impl,
+)
diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl
index d116f04..d1e3b8e 100644
--- a/tests/support/sh_py_run_test.bzl
+++ b/tests/support/sh_py_run_test.bzl
@@ -18,6 +18,7 @@
"""
load("@rules_shell//shell:sh_test.bzl", "sh_test")
+load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility
load("//python/private:py_binary_macro.bzl", "py_binary_macro") # buildifier: disable=bzl-visibility
load("//python/private:py_binary_rule.bzl", "create_binary_rule_builder") # buildifier: disable=bzl-visibility
load("//python/private:py_test_macro.bzl", "py_test_macro") # buildifier: disable=bzl-visibility
@@ -54,9 +55,9 @@
_RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_INPUTS]
_RECONFIG_ATTRS = {
- "bootstrap_impl": attr.string(),
- "build_python_zip": attr.string(default = "auto"),
- "extra_toolchains": attr.string_list(
+ "bootstrap_impl": attrb.String(),
+ "build_python_zip": attrb.String(default = "auto"),
+ "extra_toolchains": attrb.StringList(
doc = """
Value for the --extra_toolchains flag.
@@ -65,18 +66,17 @@
toolchain.
""",
),
- "python_src": attr.label(),
- "venvs_use_declare_symlink": attr.string(),
+ "python_src": attrb.Label(),
+ "venvs_use_declare_symlink": attrb.String(),
}
def _create_reconfig_rule(builder):
builder.attrs.update(_RECONFIG_ATTRS)
- base_cfg_impl = builder.cfg.implementation.get()
- builder.cfg.implementation.set(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args))
- builder.cfg.inputs.update(_RECONFIG_INPUTS)
- builder.cfg.outputs.update(_RECONFIG_OUTPUTS)
-
+ base_cfg_impl = builder.cfg.implementation()
+ builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args))
+ builder.cfg.update_inputs(_RECONFIG_INPUTS)
+ builder.cfg.update_outputs(_RECONFIG_OUTPUTS)
return builder.build()
_py_reconfig_binary = _create_reconfig_rule(create_binary_rule_builder())