From 9d0f8d277433352dcde94c37900b69596a51450e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 6 Feb 2025 13:30:46 -0800 Subject: [PATCH 01/13] wip: moar builders --- .bazelversion | 2 +- api_notes.md | 90 +++++ notes.md | 321 ++++++++++++++++++ python/private/attributes.bzl | 115 ++++--- python/private/builders.bzl | 228 ------------- python/private/common.bzl | 5 + python/private/py_binary_rule.bzl | 20 +- python/private/py_executable.bzl | 50 +-- python/private/py_library.bzl | 15 +- python/private/py_library_rule.bzl | 6 +- python/private/py_runtime_rule.bzl | 139 ++++---- python/private/py_test_rule.bzl | 18 - python/private/rule_builders.bzl | 526 +++++++++++++++++++++++++++++ tests/scratch/BUILD.bazel | 3 + tests/scratch/defs1.bzl | 65 ++++ tests/scratch/defs2.bzl | 15 + tests/support/sh_py_run_test.bzl | 11 +- 17 files changed, 1209 insertions(+), 420 deletions(-) create mode 100644 api_notes.md create mode 100644 notes.md create mode 100644 python/private/rule_builders.bzl create mode 100644 tests/scratch/BUILD.bazel create mode 100644 tests/scratch/defs1.bzl create mode 100644 tests/scratch/defs2.bzl diff --git a/.bazelversion b/.bazelversion index c6b7980b68..cd1d2e94f3 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.x +8.0.1 diff --git a/api_notes.md b/api_notes.md new file mode 100644 index 0000000000..25148ab321 --- /dev/null +++ b/api_notes.md @@ -0,0 +1,90 @@ + + +## For rule.cfg + +optional vs rule-union vs cfg-union? + +* Optional: feels verbose. Requires extra get() calls. +* Optional: seems harder to detect value +* Rule-union: which API feels verbose. +* Cfg-Union: seems nicest? More underlying impl work though. + +``` +# optional +# Rule.cfg is type Optional[TransitionBuilder | ConfigNone | ConfigTarget] + +r = RuleBuilder() +cfg = r.cfg.get() +if : + cfg.inputs.append(...) +elif : + ... +elif : + ... +else: error() + +# rule union +# Rule has {get,set}{cfg,cfg_none,cfg_target} functions +# which() tells which is set. +# Setting one clears the others + +r = RuleBuilder() +which = r.cfg_which() +if which == "cfg": + r.cfg().inputs.append(...) +elif which == "cfg_none": + ... +elif which == "cfg_target": + ... +else: error + +# cfg union (1) +# Rule.cfg is type RuleCfgBuilder +# RuleConfigBuilder has {get,set}{implementation,none,target} +# Setting one clears the others + +r = RuleBuilder() + +if r.cfg.implementation(): + r.cfg.inputs.append(...) +elif r.cfg.none(): + ... +elif r.cfg.target(): + ... +else: + error + +# cfg-union (2) +# Make implementation attribute polymorphic +impl = r.cfg.implementation() +if impl == "none": + ... +elif impl == "target": + ... +else: # function + r.cfg.inputs.append(...) + +# cfg-union (3) +# impl attr is an Optional +impl = r.cfg.implementation.get() +... r.cfg.implementation.set(...) ... +``` + +## Copies copies everywhere + +To have a nicer API, the builders should provide mutable lists/dicts/etc. + +But, when they accept a user input, they can't tell if the value is mutable or +not. So they have to make copies. Most of the time, the values probably _will_ +be mutable (why use a builder if its not mutable?). But its an easy mistake to +overlook that a list is referring to e.g. some global instead of a local var. + +So, we could defensively copy, or just document that a mutable input is +expected, and behavior is undefined otherwise. + +Alternatively, add a function to py_internal to detect immutability, and it'll +eventually be available in some bazel release. + +## Collections of of complex objects + +Should these be exposed as the raw collection, or a wrapper? e.g. diff --git a/notes.md b/notes.md new file mode 100644 index 0000000000..59bf21f268 --- /dev/null +++ b/notes.md @@ -0,0 +1,321 @@ +NOTES + +TLDR + +Two API choices to make: +1. (a) struct vs (b) dict +2. (a) less CPU/memory vs (b) nicer ergonomics + +Question: worth worrying about CPU/memory overhead? This is just loading +phase to construct the few dozen objects that are fed into rule creation + +EXAMPLE: CPU/memory vs ergonomics examples + +``` +# 1a + 2a: struct; uses less cpu/memory; worse ergonomics +def create_custom_rule() + r = py_binary_builder() + srcs = r.attrs["srcs"].to_mutable() + r.attrs["srcs"] = srcs + srcs.default.append("//bla") + cfg = srcs.cfg.get().to_mutable() + srcs.cfg.set(cfg) + cfg.inputs.append("whatever") + +# 1a+2b: struct; uses more cpu/memory; nicer ergonomics +def create_custom_rule() + r = py_binary_builder() + srcs = r.attrs["srcs"] + srcs.default.append("//bla") + srcs.cfg.inputs.append("whatever") + return r.build() + +# 1b+2a: dict; uses less cpu/memory; worse ergonomics +def create_custom_rule(): + r = py_binary_rule_kwargs() + srcs = dict(r["attrs"]["srcs"]) + r["attrs"]["srcs"] = srcs + srcs["default"] = list(srcs["default"]) + srcs["default"].append("//bla") + cfg = dict(srcs["cfg"]) + srcs["cfg"] = cfg + cfg["inputs"] = list(cfg["inputs"]) + cfg["inputs"].append("whatever") + + return rule(**r) + +# 1b+2b: dict; uses more cpu/memory; nicer ergonomics +def create_custom_rule(): + r = py_binary_rule_kwargs() + srcs = r["attrs"]["srcs"] + srcs["default"].append("//bla") + srcs["cfg"]["inputs"].append("whatever") + return rule(**r) + +``` + +Ergonomic highlights: +* Dicts don't need the `xx.{get,set}` stuff; you can just directly + assign without a wrapper. +* Structs don't need `x[key] = list(x[key])` stuff. They can ensure their + lists/dicts are mutable themselves. + * Can somewhat absolve this: just assume things are mutable +* Structs _feel_ more "solid". Like a real API. +* Structs give us API control; dicts don't. + +--------- + +Our goals are to allow users to derive new rules based upon ours. This translates to +two things: +* Specifying an implementation function. This allows them to introduce their own + logic. (Hooking into our impl function is for another time) +* Customizing the rule() kwargs. This is necessary because a different + implementation function almost certainly requires _something_ different in the + rule() kwargs. It may be new attributes or modifications to existing + attributes; we can't know and don't care. Most other rule() kwargs they want + to automatically inherit; they are either internal details or upstream + functionality they want to automatically benefit from. + +So, we need some way to for users to intercept the rule kwargs before they're +turned into immutable objects (attr.xxx, transition(), exec_group(), etc etc) +that they can't introspect or modify. + +Were we using a more traditional language, we'd probably have classes: +``` +class PyBinaryRule(Rule): ... +class UserPyBinaryRule(PyBinary): ... +``` + +And have various methods for how different pieces are created. + +We don't have classes or inheritence, though, so we have to find other avenues. + +==================== + +A key constraint are Bazel's immutability rules. + +* Objects are mutable within the thread that creates them. +* Each bzl file evaluation is a separate thread. + +Translated: +* Assigning a list/dict (_directly or indirectly_) to a global var makes it + immutable once the bzl file is finished being evaluated. +* A work around to this limitation is to use a function/lambda. When `foo.bzl` calls + `bar.bzl%create_builder()`, it is foo.bzl's thread creating _new_ objects, so + they are returned as mutable objects. + +Relatedly, this means mutability has to be "top down". e.g. given +`x: dict[str, dict[str, int]] = ...`, in order to modify +the inner dict, the outer dict must also be mutable. It's not possible +for an immutable object to reference a mutable object because Bazel +makes things recursively immutable when the thread ends. + +What this means for us: + +1. In order for us to expose objects users can modify, we _must_ provide them + with a function to create the objects they will modify. How they call that + function is up to us and defines our public API for this. +2. Whatever we expose, we cannot return immutable objects, e.g. `attr.string()`, + `transition()`, `exec_group()`, et al, or direct references to e.g. global + vars. Such objects are immutable, many cannot be introspected, and + immutability can't be detected; this prevents a user from customizing. + +==================== + +Unfortunately, everything we're dealing with is some sort of container whose +contents users may want to arbitrarily modify. A type-wise description +looks something like: + +``` +class Rule: + implementation: function + test: bool | unset + attrs: dict[str name, Attribute] + cfg: string | ExecGroup | Transition + +class LabelListAttribute(Attribute): + default: list[string | Label] + cfg: string | ExecGroup | Transition + +class Transition: + implementation: function + inputs: list[string] + outputs: list[string] + +``` + +Where calling e.g `Rule()` can be translated to using `struct(...)` or +`dict(...)` in Starlark. + +All these containers of values mean the top-down immutability rules are +prominent and affect the API. Lets discuss that next. + +==================== + +Recall: + +* Deep immutable: after calling `x = py_executable_builder()` + the result is a "mutable rule" object. Every part (i.e. dict/lists) of `x` + is mutable, recursively. e.g. `x.foo["y"].z.append(1)` works. +* Shallow immutable: after calling `x = py_executable_builder()`, + the result is a "mutable rule" object, but only the attributes/objects that + directly belong to it are _guaranteed_ mutable. e.g. + * works: `x.foo["y"] = ...` + * may not work: `x.foo["y"].z.append(1)` + +If it's deep mutable, then the user API is easy, but it costs more CPU to +create and costs more memory (equivalent objects are created multiple times). + +If it's shallow mutable, then the user API is harder. To allow mutability, +objects must provide a lambda to re-create themselves. Being a "builder" isn't +sufficient; it must have been created in the current thread context. + +Let's explore the implications of shallow vs deep immutability. + +1. Everything always deep immutable + +Each rule calls `create_xxx_rule_builder()`, the result is deep mutable. +* Pro: Easy +* Con: Wasteful. Most things aren't customized. Equivalent attributes, + transitions, etc objects get recreated instead of reused (Bazel doesn't do + anything smart with them when they're logically equivalent, and it can't + because each call carries various internal debug state info) + +2. Shallow immutability + +The benefit shallow immutability brings is objects (e.g. attr.xxx etc) are +only recreated when they're modified. + +Each rule calls `create_xxx_rule_builder`, the result is shallow mutable. e.g. +`x.attrs` is a mutable dict, but the values may not be mutable. If we want +to modify something deeper, the object has its `to_mutable()` method called. +This create a mutable version of the object (shallow or deep? read on). + +Under the hood, the way this works is objects have an immutable version of +themselves and function to create an equivalent mutable version of themselves. +e.g., creating an attribute looks like this: +``` +def Attr(builder_factory): + builder = builder_factory() + built = builder.build() + return struct(built=built, to_mutable = builder_factory) + +def LabelListBuilder(default): + self = struct( + default = default + to_mutable = lambda: self + build = lambda: attr.label(default=self.default) + ) + return self + +SRCS = Attr(lambda: LabelListBuilder(default=["a"])) +def base(): + builder.attrs["srcs"] = SRCS + +def custom(): + builder = base() + srcs = builder.attrs["srcs"].to_mutable() + srcs["default"].append("b") + builder.attrs["srcs"] = srcs +``` + +When the final `rule()` kwargs are created, the logic checks for obj.built and +uses it if present. Otherwise it calls e.g. `obj.build()` to create it. + +The disadvantage is the API is more complicated. You have to remember to call +`to_mutable()` and reassign the value. + +If the return value of `to_mutable()` is deep immutable, then this is as +complicated as the API gets. You just call it once, at the "top". + +If the return value of `to_mutable()` is _also_ shallow mutable, then this is +API complication is recursive in nature. e.g, lets say we want to modify the +inputs for an attributes's transition when things are shallow immutable: + +``` +def custom(): + builder = base() + srcs = builder.attrs["srcs"].to_mutable() # -> LabelListBuilder + cfg = srcs.cfg.to_mutable() # TransitionBuilder + cfg.inputs.append("bla") + srcs.cfg.set(cfg) # store our modified cfg back into the attribute + builder.attrs["srcs"] = srcs # store modified attr back into the rule attrs +``` + +Pretty tedious. + +Also, the nature of the top-down mutability constraint somewhat works against +the design goal here. We avoid having to recreate _all_ the objects for a rule, +but we still had to re-create the direct values that the srcs Attribute object +manages. So less work, but definitely not precise. + +3. Mix/Match of immutability + +A compromise between (1) and (2) is for `to_mutable()` to be shallow for some +things but deep for others. + +* Rule is shallow immutable. e.g. `Rule.attrs` is a mutable dict, but contains + immutable Attribute objects. +* Attribute.to_mutable returns a deep mutable object. This avoids having to + call to_mutable() many times and reassign up the object tree. + +-------------------- + +Alternative: visitor pattern + +Instead of returning a mutable value to users to modify, users pass in a +Visitor object, which has methods to handle rule kwarg building. e.g. + +``` +SRCS = Attr(lambda: LabelListBuilder(...)) + +def create_executable_rule(visitor): + kwargs = visitor.init_kwargs(visitor) + kwargs["srcs"] = visitor.add_attr("srcs", SRCS.built, SRCS.to_mutable) + kwargs = visitor.finalize_kwargs(kwargs) + return rule(**kwargs) + +def visitor(): + return struct( + add_attr = visit_add_attr, + ... + ) + +def customize_add_attr(name, built, new_builder): + if name != "srcs": + return built + builder = new_builder() + builder.default.append("custom") + return builder.build() + +custom_rule = create_executable_rule(visitor()) +``` + +Unfortunately, this doesn't change things too much. The same issue of object +reuse vs immutability show up. This actually seems _worse_ because now +a user has to do pseudo-object-oriented Starlark for even small changes. + + +-------------------- + +Alternative: overrride values + +The idea here is to return immutable values to benefit from better cpu/memory +usage. To modify something, a user calls a function to create a mutable version +of it, then overwrites the value entirely. + +``` +load(":base.bzl", "create_srcs_attr_builder", "create_cfg_builder") +def custom_rule(): + builder = base() + srcs = create_srcs_attr_builder() + srcs.providers.append("bla") + + cfg = create_cfg_builder() + cfg.inputs.append("bla") + builder.cfg.set(cfg) +``` + +This is similar to having a `to_mutable()` method. The difference is there are +no wrapper objects in between. e.g. the builder.attrs dict contains attr.xxx +objects, instead of `struct(built=, to_mutable=)` diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index e167482eb1..dfec41a43e 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -13,14 +13,17 @@ # 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(":builders.bzl", "builders") load(":common.bzl", "union_attrs") 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", "rule_builders") load( ":semantics.bzl", "DEPS_ATTR_ALLOW_RULES", @@ -141,7 +144,7 @@ PycCollectionAttr = enum( def create_stamp_attr(**kwargs): return { - "stamp": attr.int( + "stamp": lambda: rule_builders.IntAttrBuilder( values = _STAMP_VALUES, doc = """ Whether to encode build information into the binary. Possible values: @@ -164,26 +167,12 @@ be avoided if possible. } 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`. -""", - ), - } + fail("hit") -SRCS_VERSION_ALL_VALUES = ["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"] -SRCS_VERSION_NON_CONVERSION_VALUES = ["PY2AND3", "PY2ONLY", "PY3ONLY"] +SRCS_VERSION_ALL_VALUES = [] ##["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"] def create_srcs_version_attr(values): + fail("hit") return { "srcs_version": attr.string( default = "PY2AND3", @@ -216,7 +205,7 @@ CC_TOOLCHAIN = { 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: rule_builders.LabelListAttrBuilder( allow_files = True, flags = ["SKIP_CONSTRAINTS_OVERRIDE"], doc = """ @@ -244,7 +233,7 @@ def _create_native_rules_allowlist_attrs(): providers = [] return { - "_native_rules_allowlist": attr.label( + "_native_rules_allowlist": lambda: rule_builders.LabelAttrBuilder( default = default, providers = providers, ), @@ -253,25 +242,24 @@ def _create_native_rules_allowlist_attrs(): 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 { # NOTE: This attribute is deprecated and slated for removal. - "distribs": attr.string_list(), + ##"distribs": attr.string_list(), # TODO(b/148103851): This attribute is deprecated and slated for # removal. # NOTE: The license attribute is missing in some Java integration tests, # so fallback to a regular string_list for that case. # buildifier: disable=attr-license - "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), + ##"licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), }, - allow_none = True, ) IMPORTS_ATTRS = { - "imports": attr.string_list( + "imports": lambda: rule_builders.StringListAttrBuilder( doc = """ List of import directories to be added to the PYTHONPATH. @@ -289,9 +277,9 @@ above the execution root are not allowed and will result in an error. _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: rule_builders.LabelListAttrBuilder( providers = [ [PyInfo], [CcInfo], @@ -310,7 +298,7 @@ Targets that only provide data files used at runtime belong in the `data` attribute. """, ), - "precompile": attr.string( + "precompile": lambda: rule_builders.StringAttrBuilder( doc = """ Whether py source files **for this target** should be precompiled. @@ -332,7 +320,7 @@ Values: default = PrecompileAttr.INHERIT, values = sorted(PrecompileAttr.__members__.values()), ), - "precompile_invalidation_mode": attr.string( + "precompile_invalidation_mode": lambda: rule_builders.StringAttrBuilder( doc = """ How precompiled files should be verified to be up-to-date with their associated source files. Possible values are: @@ -350,7 +338,7 @@ https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode default = PrecompileInvalidationModeAttr.AUTO, values = sorted(PrecompileInvalidationModeAttr.__members__.values()), ), - "precompile_optimize_level": attr.int( + "precompile_optimize_level": lambda: rule_builders.IntAttrBuilder( doc = """ The optimization level for precompiled files. @@ -363,7 +351,7 @@ runtime when the code actually runs. """, default = 0, ), - "precompile_source_retention": attr.string( + "precompile_source_retention": lambda: rule_builders.StringAttrBuilder( default = PrecompileSourceRetentionAttr.INHERIT, values = sorted(PrecompileSourceRetentionAttr.__members__.values()), doc = """ @@ -375,7 +363,7 @@ in the resulting output or not. Valid values are: * `omit_source`: Don't include the original py source. """, ), - "pyi_deps": attr.label_list( + "pyi_deps": lambda: rule_builders.LabelListAttrBuilder( doc = """ Dependencies providing type definitions the library needs. @@ -391,7 +379,7 @@ program (packaging rules may include them, however). [CcInfo], ] + _MaybeBuiltinPyInfo, ), - "pyi_srcs": attr.label_list( + "pyi_srcs": lambda: rule_builders.LabelListAttrBuilder( doc = """ Type definition files for the library. @@ -406,35 +394,65 @@ as part of a runnable program (packaging rules may include them, however). ), # Required attribute, but details vary by rule. # Use create_srcs_attr to create one. - "srcs": None, + "srcs": lambda: rule_builders.LabelListAttrBuilder( + # 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`. +""", + ), # 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_version": None, + "srcs_version": lambda: rule_builders.StringAttrBuilder( + doc = "Defunct, unused, does nothing.", + ), + "_precompile_flag": lambda: rule_builders.LabelAttrBuilder( default = "//python/config_settings:precompile", providers = [BuildSettingInfo], ), - "_precompile_source_retention_flag": attr.label( + "_precompile_source_retention_flag": lambda: rule_builders.LabelAttrBuilder( 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: rule_builders.BoolAttrBuilder( + 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: rule_builders.LabelAttrBuilder( + 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: rule_builders.LabelAttrBuilder( + 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: rule_builders.StringDictAttrBuilder( doc = """\ Dictionary of strings; optional; values are subject to `$(location)` and "Make variable" substitution. @@ -445,20 +463,19 @@ Specifies additional environment variables to set when the target is executed by ), # The value is required, but varies by rule and/or rule type. Use # create_stamp_attr to create one. - "stamp": None, + ##"stamp": None, }, - 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_TEST_ATTRS = dicts.add( AGNOSTIC_EXECUTABLE_ATTRS, # Tests have stamping disabled by default. create_stamp_attr(default = 0), { - "env_inherit": attr.string_list( + "env_inherit": lambda: rule_builders.StringListAttrBuilder( doc = """\ List of strings; optional @@ -467,7 +484,7 @@ environment when the test is executed by bazel test. """, ), # TODO(b/176993122): Remove when Bazel automatically knows to run on darwin. - "_apple_constraints": attr.label_list( + "_apple_constraints": lambda: rule_builders.LabelListAttrBuilder( default = [ "@platforms//os:ios", "@platforms//os:macos", @@ -482,7 +499,7 @@ environment when the test is executed by bazel test. # 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_BINARY_ATTRS = dicts.add( AGNOSTIC_EXECUTABLE_ATTRS, create_stamp_attr(default = -1), ) diff --git a/python/private/builders.bzl b/python/private/builders.bzl index bf5dbb8667..50aa3ed91a 100644 --- a/python/private/builders.bzl +++ b/python/private/builders.bzl @@ -96,145 +96,6 @@ def _DepsetBuilder_build(self): 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 @@ def _RunfilesBuilder_build(self, ctx, **kwargs): **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 @@ def _is_runfiles(value): builders = struct( DepsetBuilder = _DepsetBuilder, RunfilesBuilder = _RunfilesBuilder, - RuleBuilder = _RuleBuilder, - TransitionBuilder = _TransitionBuilder, - SetBuilder = _SetBuilder, - Optional = _Optional, ) diff --git a/python/private/common.bzl b/python/private/common.bzl index 137f0d23f3..8b0ce7486c 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -227,7 +227,12 @@ def union_attrs(*attr_dicts, allow_none = False): Returns: dict of attributes. """ + + # todo: probably remove this entirely? is kind of annoying logic to have. result = {} + for other in attr_dicts: + result.update(other) + return result missing = {} for attr_dict in attr_dicts: for attr_name, value in attr_dict.items(): diff --git a/python/private/py_binary_rule.bzl b/python/private/py_binary_rule.bzl index 5b40f52198..9fa9061111 100644 --- a/python/private/py_binary_rule.bzl +++ b/python/private/py_binary_rule.bzl @@ -14,29 +14,13 @@ """Rule implementation of py_binary for Bazel.""" load(":attributes.bzl", "AGNOSTIC_BINARY_ATTRS") +load(":builders.bzl", "builders") load( ":py_executable.bzl", "create_executable_rule_builder", "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 +34,7 @@ def create_binary_rule_builder(): executable = True, ) builder.attrs.update(AGNOSTIC_BINARY_ATTRS) - builder.attrs.update(_COVERAGE_ATTRS) + builder.attrs.get("srcs").doc.set("asdf") return builder py_binary = create_binary_rule_builder().build() diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index a2ccdc65f3..af03187311 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -22,6 +22,7 @@ load( ":attributes.bzl", "AGNOSTIC_EXECUTABLE_ATTRS", "COMMON_ATTRS", + "COVERAGE_ATTRS", "IMPORTS_ATTRS", "PY_SRCS_ATTRS", "PrecompileAttr", @@ -60,6 +61,7 @@ load(":py_info.bzl", "PyInfo") load(":py_internal.bzl", "py_internal") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") +load(":rule_builders.bzl", "rule_builders") load( ":semantics.bzl", "ALLOWED_MAIN_EXTENSIONS", @@ -87,13 +89,14 @@ _CC_TOOLCHAINS = [config_common.toolchain_type( # 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: rule_builders.IntAttrBuilder( default = -1, values = [-1, 0, 1], doc = """\ @@ -110,7 +113,7 @@ the `srcs` of Python targets as required. # 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: rule_builders.LabelAttrBuilder( allow_single_file = True, doc = """\ Optional; the name of the source file that is the main entry point of the @@ -119,7 +122,7 @@ application. This file must also be listed in `srcs`. If left unspecified, filename in `srcs`, `main` must be specified. """, ), - "pyc_collection": attr.string( + "pyc_collection": lambda: rule_builders.StringAttrBuilder( default = PycCollectionAttr.INHERIT, values = sorted(PycCollectionAttr.__members__.values()), doc = """ @@ -134,7 +137,7 @@ Valid values are: target level. """, ), - "python_version": attr.string( + "python_version": lambda: rule_builders.StringAttrBuilder( # TODO(b/203567235): In the Java impl, the default comes from # --python_version. Not clear what the Starlark equivalent is. doc = """ @@ -160,25 +163,25 @@ accepting arbitrary Python versions. """, ), # Required to opt-in to the transition feature. - "_allowlist_function_transition": attr.label( + "_allowlist_function_transition": lambda: rule_builders.LabelAttrBuilder( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), - "_bootstrap_impl_flag": attr.label( + "_bootstrap_impl_flag": lambda: rule_builders.LabelAttrBuilder( default = "//python/config_settings:bootstrap_impl", providers = [BuildSettingInfo], ), - "_bootstrap_template": attr.label( + "_bootstrap_template": lambda: rule_builders.LabelAttrBuilder( allow_single_file = True, default = "@bazel_tools//tools/python:python_bootstrap_template.txt", ), - "_launcher": attr.label( + "_launcher": lambda: rule_builders.LabelAttrBuilder( 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: rule_builders.LabelAttrBuilder( # 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. @@ -190,35 +193,35 @@ accepting arbitrary Python versions. # TODO: This appears to be vestigial. It's only added because # GraphlessQueryTest.testLabelsOperator relies on it to test for # query behavior of implicit dependencies. - "_py_toolchain_type": attr.label( - default = TARGET_TOOLCHAIN_TYPE, - ), - "_python_version_flag": attr.label( + ##"_py_toolchain_type": attr.label( + ## default = TARGET_TOOLCHAIN_TYPE, + ##), + "_python_version_flag": lambda: rule_builders.LabelAttrBuilder( default = "//python/config_settings:python_version", ), - "_venvs_use_declare_symlink_flag": attr.label( + "_venvs_use_declare_symlink_flag": lambda: rule_builders.LabelAttrBuilder( default = "//python/config_settings:venvs_use_declare_symlink", providers = [BuildSettingInfo], ), - "_windows_constraints": attr.label_list( + "_windows_constraints": lambda: rule_builders.LabelListAttrBuilder( default = [ "@platforms//os:windows", ], ), - "_windows_launcher_maker": attr.label( + "_windows_launcher_maker": lambda: rule_builders.LabelAttrBuilder( default = "@bazel_tools//tools/launcher:launcher_maker", cfg = "exec", executable = True, ), - "_zipper": attr.label( + "_zipper": lambda: rule_builders.LabelAttrBuilder( 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, + ##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,7 +1750,7 @@ def create_base_executable_rule(): return create_executable_rule_builder().build() def create_executable_rule_builder(implementation, **kwargs): - builder = builders.RuleBuilder( + builder = rule_builders.RuleBuilder( implementation = implementation, attrs = EXECUTABLE_ATTRS, exec_groups = REQUIRED_EXEC_GROUPS, @@ -1757,13 +1760,14 @@ def create_executable_rule_builder(implementation, **kwargs): TOOLCHAIN_TYPE, config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ] + _CC_TOOLCHAINS, - cfg = builders.TransitionBuilder( + cfg = rule_builders.RuleCfgBuilder( implementation = _transition_executable_impl, inputs = [_PYTHON_VERSION_FLAG], outputs = [_PYTHON_VERSION_FLAG], ), **kwargs ) + builder.attrs.get("srcs").mandatory.set(True) return builder def cc_configure_features( diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index 350ea35aa6..a837620304 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -40,6 +40,7 @@ load( load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_internal.bzl", "py_internal") +load(":rule_builders.bzl", "rule_builders") load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", @@ -48,12 +49,12 @@ load( _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), + ##create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES), + ##create_srcs_attr(mandatory = False), { "_add_srcs_to_runfiles_flag": attr.label( default = "//python/config_settings:add_srcs_to_runfiles", @@ -145,7 +146,7 @@ Source files are no longer added to the runfiles directly. ::: """ -def create_py_library_rule(*, attrs = {}, **kwargs): +def create_py_library_rule_builder(*, attrs = {}, **kwargs): """Creates a py_library rule. Args: @@ -162,12 +163,14 @@ def create_py_library_rule(*, attrs = {}, **kwargs): # RequiredConfigFragmentsTest passes fragments = kwargs.pop("fragments", None) or [] kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {}) - return rule( + + builder = rule_builders.RuleBuilder( attrs = dicts.add(LIBRARY_ATTRS, attrs), + fragments = fragments + ["py"], toolchains = [ config_common.toolchain_type(TOOLCHAIN_TYPE, mandatory = False), config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ], - fragments = fragments + ["py"], **kwargs ) + return builder diff --git a/python/private/py_library_rule.bzl b/python/private/py_library_rule.bzl index 8a8d6cf380..44382a76d6 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 @@ def _py_library_impl_with_semantics(ctx): ), ) -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 5ce8161cf0..9407cac50f 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -188,19 +188,21 @@ py_runtime( ``` """, fragments = ["py"], - attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, { - "abi_flags": attr.string( - default = "", - doc = """ + attrs = dicts.add( + {k: v().build() for k, v in NATIVE_RULES_ALLOWLIST_ATTRS.items()}, + { + "abi_flags": attr.string( + default = "", + 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 @@ itself. 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 @@ The entry point for the tool must be loadable by a Python interpreter (e.g. a 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 @@ can be either of: 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 @@ values are strings, most are converted to ints. The supported keys are: {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 @@ The default value is controlled by the `--incompatible_py3_is_default` flag. 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 @@ motivation. 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 @@ This becomes the entry point executed when `python foo.zip` is run. 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 6ad4fbddb8..72e8bab805 100644 --- a/python/private/py_test_rule.bzl +++ b/python/private/py_test_rule.bzl @@ -21,23 +21,6 @@ load( "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 @@ def create_test_rule_builder(): 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 0000000000..eb73b315ac --- /dev/null +++ b/python/private/rule_builders.bzl @@ -0,0 +1,526 @@ +"""Builders specific for creating rules, aspects, attributes et al.""" + +load("@bazel_skylib//lib:types.bzl", "types") +load(":builders.bzl", "builders") + +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: Either zero, one, or two positional args to set the + initial value stored for the optional. + If zero args, then no value is stored. + If one arg, then the arg is the value stored. + If two args, then the first arg is a kwargs dict, and the + second arg is a name in kwargs to look for. If the name is + present in kwargs, it is removed from kwargs and its value + stored, otherwise kwargs is unmodified and no value is stored. + + Returns: + {type}`Optional` + """ + if len(initial) > 2: + fail("Only zero, one, or two positional args allowed, but got: {}".format(initial)) + + if len(initial) == 2: + kwargs, name = initial + if name in kwargs: + initial = kwargs.pop(name) + else: + initial = () + + # 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 _is_optional(obj): + return hasattr(obj, "present") + +def _RuleCfgBuilder(**kwargs): + self = struct( + _implementation = [kwargs.pop("implementation", None)], + set_implementation = lambda *a, **k: _RuleCfgBuilder_set_implementation(self, *a, **k), + implementation = lambda: _RuleCfgBuilder(self), + outputs = _SetBuilder(_kwargs_pop_list(kwargs, "outputs")), + inputs = _SetBuilder(_kwargs_pop_list(kwargs, "inputs")), + build = lambda *a, **k: _RuleCfgBuilder_build(self, *a, **k), + ) + return self + +def _RuleCfgBuilder_set_implementation(self, value): + self._implementation[0] = value + +def _RuleCfgBuilder_implementation(self): + """Returns the implementation name or function for the cfg transition. + + Returns: + {type}`str | function` + """ + return self._implementation[0] + +def _RuleCfgBuilder_build(self): + impl = self._implementation[0] + + # todo: move these strings into an enum + if impl == "target" or impl == None: + return config.target() + elif impl == "none": + return config.none() + elif types.is_function(impl): + return transition( + implementation = impl, + inputs = self.inputs.build(), + outputs = self.outputs.build(), + ) + else: + return impl + +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 = _AttrsDict(kwargs.pop("attrs", None)), + cfg = kwargs.pop("cfg", None) or _RuleCfgBuilder(), + # todo: create ExecGroupBuilder (allows mutation) or ExecGroup (allows introspection) + exec_groups = _kwargs_pop_dict(kwargs, "exec_groups"), + executable = _Optional(kwargs, "executable"), + fragments = list(kwargs.pop("fragments", None) or []), + implementation = _Optional(implementation), + extra_kwargs = kwargs, + provides = _kwargs_pop_list(kwargs, "provides"), + test = _Optional(kwargs, "test"), + # todo: create ToolchainTypeBuilder or ToolchainType + toolchains = _kwargs_pop_list(kwargs, "toolchains"), + build = lambda *a, **k: _RuleBuilder_build(self, *a, **k), + to_kwargs = lambda *a, **k: _RuleBuilder_to_kwargs(self, *a, **k), + ) + 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 _Builder_get_pairs(kwargs, obj): + ignore_names = {"extra_kwargs": None} + pairs = [] + for name in dir(obj): + if name in ignore_names or name in kwargs: + continue + value = getattr(obj, name) + if types.is_function(value): + continue # Assume it's a method + if _is_optional(value): + if not value.present(): + continue + else: + value = value.get() + + # NOTE: We can't call value.build() here: it would likely lead to + # recursion. + pairs.append((name, value)) + return pairs + +def _Builder_to_kwargs_nobuilders(self, kwargs = None): + if kwargs == None: + kwargs = {} + kwargs.update(self.extra_kwargs) + for name, value in _Builder_get_pairs(kwargs, self): + kwargs[name] = value + return kwargs + +def _RuleBuilder_to_kwargs(self): + """Builds the arguments for calling `rule()`. + + Args: + self: implicitly added + + Returns: + {type}`dict` + """ + kwargs = dict(self.extra_kwargs) + for name, value in _Builder_get_pairs(kwargs, self): + value = value.build() if hasattr(value, "build") else value + kwargs[name] = value + return kwargs + +def _AttrsDict(initial): + self = struct( + values = {}, + update = lambda *a, **k: _AttrsDict_update(self, *a, **k), + get = lambda *a, **k: self.values.get(*a, **k), + items = lambda: self.values.items(), + build = lambda: _AttrsDict_build(self), + ) + if initial: + _AttrsDict_update(self, initial) + return self + +def _AttrsDict_update(self, other): + for k, v in other.items(): + if types.is_function(v): + self.values[k] = v() + else: + self.values[k] = v + +def _AttrsDict_build(self): + attrs = {} + for k, v in self.values.items(): + if hasattr(v, "build"): + v = v.build() + if not type(v) == "Attribute": + fail("bad attr type:", k, type(v), v) + attrs[k] = v + return attrs + +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` + """ + + # todo: accept string | exec_group | config.name | config.target | + # transition + + # 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 + ) + +def _kwargs_pop_dict(kwargs, key): + return dict(kwargs.pop(key, None) or {}) + +def _kwargs_pop_list(kwargs, key): + return list(kwargs.pop(key, None) or []) + +def _BoolAttrBuilder(**kwargs): + """Create a builder for attributes. + + Returns: + {type}`BoolAttrBuilder` + """ + + # buildifier: disable=uninitialized + self = struct( + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + extra_kwargs = kwargs, + build = lambda: attr.bool(**_Builder_to_kwargs_nobuilders(self)), + ) + return self + +def _IntAttrBuilder(**kwargs): + # buildifier: disable=uninitialized + self = struct( + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + values = kwargs.get("values") or [], + build = lambda *a, **k: _IntAttrBuilder_build(self, *a, **k), + extra_kwargs = kwargs, + ) + return self + +def _IntAttrBuilder_build(self): + kwargs = _Builder_to_kwargs_nobuilders(self) + return attr.int(**kwargs) + +def _AttrCfgBuilder(**kwargs): + # todo: For attributes, cfg can be: + # string | transition | config.exec(...) | config.target() | config.none() + self = struct( + _implementation = [None], + implementation = lambda: self._implementation[0], + set_implementation = lambda *a, **k: _AttrCfgBuilder_set_implementation(self, *a, **k), + outputs = _SetBuilder(_kwargs_pop_list(kwargs, "outputs")), + inputs = _SetBuilder(_kwargs_pop_list(kwargs, "inputs")), + build = lambda: _AttrCfgBuilder_build(self), + ) + return self + +def _AttrCfgBuilder_set_implementation(self, value): + self._implementation[0] = value + +def _AttrCfgBuilder_build(self): + impl = self._implementation[0] + if impl == None: + return None + elif impl == "target": + return config.target() + elif impl == "exec": + return config.exec() + elif impl == "???": + return config.exec(impl) + elif types.is_function(impl): + return transition( + implementation = impl, + inputs = self.inputs.build(), + outputs = self.outputs.build(), + ) + else: + return impl + +def _LabelAttrBuilder(**kwargs): + # buildifier: disable=uninitialized + self = struct( + # value or configuration_field + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + executable = _Optional(kwargs, "executable"), + # True, False, or list + allow_files = _Optional(kwargs, "allow_files"), + allow_single_file = _Optional(kwargs, "allow_single_file"), + providers = kwargs.pop("providers", None) or [], + cfg = _Optional(kwargs, "cfg"), + aspects = kwargs.pop("aspects", None) or [], + build = lambda *a, **k: _LabelAttrBuilder_build(self, *a, **k), + extra_kwargs = kwargs, + ) + return self + +def _LabelAttrBuilder_build(self): + kwargs = { + "aspects": [v.build() for v in self.aspects], + } + _Builder_to_kwargs_nobuilders(self, kwargs) + for name, value in kwargs.items(): + kwargs[name] = value.build() if hasattr(value, "build") else value + return attr.label(**kwargs) + +def _LabelListAttrBuilder(**kwargs): + self = struct( + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + executable = _Optional(kwargs, "executable"), + allow_empty = _Optional(kwargs, "allow_empty"), + # True, False, or list + allow_files = _Optional(kwargs, "allow_files"), + providers = kwargs.pop("providers", None) or [], + # string, config.exec_group, config.none, config.target, or transition + # For the latter, it's a builder + cfg = _Optional(kwargs, "cfg"), + aspects = kwargs.pop("aspects", None) or [], + build = lambda *a, **k: attr.label_list(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + extra_kwargs = kwargs, + ) + return self + +def _StringListAttrBuilder(**kwargs): + self = struct( + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + allow_empty = _Optional(kwargs, "allow_empty"), + # True, False, or list + build = lambda *a, **k: attr.string_list(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + extra_kwargs = kwargs, + ) + return self + +def _StringAttrBuilder(**kwargs): + self = struct( + default = _Optional(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + # True, False, or list + allow_empty = _Optional(kwargs, "allow_empty"), + build = lambda *a, **k: attr.string(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + extra_kwargs = kwargs, + values = kwargs.get("values") or [], + ) + return self + +def _StringDictAttrBuilder(**kwargs): + self = struct( + default = _kwargs_pop_dict(kwargs, "default"), + doc = _Optional(kwargs, "doc"), + mandatory = _Optional(kwargs, "mandatory"), + allow_empty = _Optional(kwargs, "allow_empty"), + build = lambda: attr.string_dict(**_Builder_to_kwargs_nobuilders(self)), + extra_kwargs = kwargs, + ) + return self + +def _Buildable(builder_factory, kwargs_fn = None, ATTR = None, ABSTRACT = False): + if kwargs_fn: + kwargs = kwargs_fn() + built = builder_factory(**kwargs_fn()) + to_builder = struct(build_kwargs = builder_factory, kwargs_fn = kwargs_fn) + else: + to_builder = builder_factory + if not ABSTRACT: + builder = builder_factory() + if hasattr(builder, "build"): + built = builder.build() + elif types.is_dict(builder) and "@build" in builder: + built = builder["@build"](**{k: v for k, v in builder.items() if k != "@build"}) + elif hasattr(builder, "build_kwargs"): + built = builder.build_kwargs(**builder.kwargs_fn()) + else: + fail("bad builder factory:", builder_factory, "->", builder) + if ABSTRACT: + return struct( + build = to_builder().build, # might be recursive issue? + to_builder = to_builder, + ) + return struct( + built = built, + to_builder = to_builder, + ) + +rule_builders = struct( + RuleBuilder = _RuleBuilder, + TransitionBuilder = _TransitionBuilder, + SetBuilder = _SetBuilder, + Optional = _Optional, + LabelAttrBuilder = _LabelAttrBuilder, + LabelListAttrBuilder = _LabelListAttrBuilder, + Buildable = _Buildable, + IntAttrBuilder = _IntAttrBuilder, + StringListAttrBuilder = _StringListAttrBuilder, + StringAttrBuilder = _StringAttrBuilder, + StringDictAttrBuilder = _StringDictAttrBuilder, + BoolAttrBuilder = _BoolAttrBuilder, + RuleCfgBuilder = _RuleCfgBuilder, +) diff --git a/tests/scratch/BUILD.bazel b/tests/scratch/BUILD.bazel new file mode 100644 index 0000000000..a017ce2453 --- /dev/null +++ b/tests/scratch/BUILD.bazel @@ -0,0 +1,3 @@ +load(":defs1.bzl", "defs1") + +defs1() diff --git a/tests/scratch/defs1.bzl b/tests/scratch/defs1.bzl new file mode 100644 index 0000000000..ab17190262 --- /dev/null +++ b/tests/scratch/defs1.bzl @@ -0,0 +1,65 @@ +load(":defs2.bzl", "D", "THING") + +def recursive_build(top): + top_res = {} + + def store_final(nv): + top_res["FINAL"] = nv + + stack = [(top.build, store_final)] + for _ in range(10000): + if not stack: + break + f, store = stack.pop() + f(stack, store) + print("topres=", top_res) + +def Builder(**kwargs): + self = struct( + kwargs = {} | kwargs, + build = lambda *a, **k: _build(self, *a, **k), + ) + return self + +def ListBuilder(*args): + self = struct( + values = list(args), + build = lambda *a, **k: _build_list(self, *a, **k), + ) + return self + +def _build(self, stack, store_result): + result = {} + for k, v in self.kwargs.items(): + if hasattr(v, "build"): + stack.append((v.build, (lambda nv, k = k: _set(result, k, nv)))) + else: + result[k] = v + + store_result(result) + +def _build_list(self, stack, store_result): + list_result = [] + for v in self.values: + if hasattr(v, "build"): + stack.append(v.build, lambda nv: list_result.append(nv)) + else: + list_result.append(v) + store_result(list_result) + +def _set(o, k, v): + o[k] = v + +def defs1(): + top = Builder( + a = Builder( + a1 = True, + ), + b = Builder( + b1 = 2, + b2 = ListBuilder(1, 2, 3), + ), + ) + + todo = [] + recursive_build(top) diff --git a/tests/scratch/defs2.bzl b/tests/scratch/defs2.bzl new file mode 100644 index 0000000000..31eab4c652 --- /dev/null +++ b/tests/scratch/defs2.bzl @@ -0,0 +1,15 @@ +def AttrBuilder(values): + return struct( + values = values, + ) + +def Attr(builder_factory): + return struct( + built = builder_factory(), + to_builder = lambda: builder_factory(), + ) + +NEW_THING_BUILDER = lambda: AttrBuilder(values = ["asdf"]) +THING = Attr(NEW_THING_BUILDER) + +D = {"x": None} diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index d116f0403f..d215ba7347 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -69,17 +69,16 @@ toolchain. "venvs_use_declare_symlink": attr.string(), } -def _create_reconfig_rule(builder): +def _create_reconfig_rule(builder, is_bin = False): 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)) + base_cfg_impl = builder.cfg.implementation() + builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args)) builder.cfg.inputs.update(_RECONFIG_INPUTS) builder.cfg.outputs.update(_RECONFIG_OUTPUTS) + return builder.build(debug = "reconfig") - return builder.build() - -_py_reconfig_binary = _create_reconfig_rule(create_binary_rule_builder()) +_py_reconfig_binary = _create_reconfig_rule(create_binary_rule_builder(), True) _py_reconfig_test = _create_reconfig_rule(create_test_rule_builder()) From 31683220cf837d1d05d6762c07ee20fdae0edadf Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 23 Feb 2025 20:39:18 -0800 Subject: [PATCH 02/13] revert bazelversion file change --- .bazelversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelversion b/.bazelversion index cd1d2e94f3..c6b7980b68 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.0.1 +8.x From 24aab23169e811094c3446b315546c5161ce05f9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 23 Feb 2025 21:01:54 -0800 Subject: [PATCH 03/13] fix bug in optional construciton, add some docs, fix RuleCfgBuilder.implementation --- python/private/rule_builders.bzl | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index eb73b315ac..d947cd56a9 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -28,13 +28,16 @@ def _Optional(*initial): if len(initial) == 2: kwargs, name = initial if name in kwargs: - initial = kwargs.pop(name) + initial = [kwargs.pop(name)] else: - initial = () + initial = [] + else: + initial = list(initial) # buildifier: disable=uninitialized self = struct( - _value = list(initial), + # Length zero when no value; length one when has value. + _value = 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), @@ -84,7 +87,7 @@ def _RuleCfgBuilder(**kwargs): self = struct( _implementation = [kwargs.pop("implementation", None)], set_implementation = lambda *a, **k: _RuleCfgBuilder_set_implementation(self, *a, **k), - implementation = lambda: _RuleCfgBuilder(self), + implementation = lambda: _RuleCfgBuilder_implementation(self), outputs = _SetBuilder(_kwargs_pop_list(kwargs, "outputs")), inputs = _SetBuilder(_kwargs_pop_list(kwargs, "inputs")), build = lambda *a, **k: _RuleCfgBuilder_build(self, *a, **k), @@ -92,6 +95,12 @@ def _RuleCfgBuilder(**kwargs): return self def _RuleCfgBuilder_set_implementation(self, value): + """Set the implementation method. + + Args: + self: implicitly added. + value: {type}`str | function` a valid `rule.cfg` argument value. + """ self._implementation[0] = value def _RuleCfgBuilder_implementation(self): @@ -185,6 +194,7 @@ def _Builder_get_pairs(kwargs, obj): pairs.append((name, value)) return pairs +# This function isn't allowed to call builders to prevent recursion def _Builder_to_kwargs_nobuilders(self, kwargs = None): if kwargs == None: kwargs = {} @@ -222,6 +232,7 @@ def _AttrsDict(initial): def _AttrsDict_update(self, other): for k, v in other.items(): + # Handle factory functions that create builders if types.is_function(v): self.values[k] = v() else: From 8c689637fa3b58f5d6493f89ceee2a8cb1e57277 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 25 Feb 2025 21:25:02 -0800 Subject: [PATCH 04/13] setup docs, bzl_library, minor cleanup --- docs/BUILD.bazel | 1 + python/private/BUILD.bazel | 13 + python/private/py_executable.bzl | 2 +- python/private/rule_builders.bzl | 658 ++++++++++++++++++++++--------- 4 files changed, 477 insertions(+), 197 deletions(-) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index ea386f114a..cb49310418 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -102,6 +102,7 @@ sphinx_stardocs( "//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/python/private/BUILD.bazel b/python/private/BUILD.bazel index 2928dab068..3e24014a6d 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -92,6 +92,15 @@ bzl_library( ], ) +bzl_library( + name = "rule_builders_bzl", + srcs = ["rule_builders.bzl"], + deps = [ + ":builders_bzl", + "@bazel_skylib//lib:types", + ], +) + bzl_library( name = "bzlmod_enabled_bzl", srcs = ["bzlmod_enabled.bzl"], @@ -283,6 +292,7 @@ bzl_library( deps = [ ":attributes_bzl", ":py_executable_bzl", + ":rule_builders_bzl", ":semantics_bzl", "@bazel_skylib//lib:dicts", ], @@ -410,6 +420,7 @@ bzl_library( ":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 +486,7 @@ bzl_library( ":py_internal_bzl", ":py_runtime_info_bzl", ":reexports_bzl", + ":rule_builders_bzl", ":util_bzl", "@bazel_skylib//lib:dicts", "@bazel_skylib//lib:paths", @@ -515,6 +527,7 @@ bzl_library( ":attributes_bzl", ":common_bzl", ":py_executable_bzl", + ":rule_builders_bzl", ":semantics_bzl", "@bazel_skylib//lib:dicts", ], diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index af03187311..b57ae47930 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -1760,7 +1760,7 @@ def create_executable_rule_builder(implementation, **kwargs): TOOLCHAIN_TYPE, config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ] + _CC_TOOLCHAINS, - cfg = rule_builders.RuleCfgBuilder( + cfg = dict( implementation = _transition_executable_impl, inputs = [_PYTHON_VERSION_FLAG], outputs = [_PYTHON_VERSION_FLAG], diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index d947cd56a9..cc7e2e3ca4 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -1,23 +1,101 @@ -"""Builders specific for creating rules, aspects, attributes et al.""" +"""Builders for creating rules, aspects, attributes 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. + +These builders are, essentially, mutable and inspectable, wrappers for those +Bazel objects. This allow defining a rule where the values are mutable and +callers can customize to derive their own variant of the rule. + +:::{important} +When using builders, all the values 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 the global variables +are frozen. +::: + +Example usage: + +``` +# File: foo_binary.bzl +def create_foo_binary_builder(): + r = RuleBuilder() + r.implementation.set(_foo_binary_impl) + r.attrs["srcs"] = LabelListAttrBuilder(...) + return r + +foo_binary = create_foo_binary_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.bzl", "builders") -def _Optional(*initial): +def _to_kwargs_get_pairs(kwargs, obj): + ignore_names = {"extra_kwargs": None} + pairs = [] + for name in dir(obj): + if name in ignore_names or name in kwargs: + continue + value = getattr(obj, name) + if types.is_function(value): + continue # Assume it's a method + if _is_optional(value): + if not value.present(): + continue + else: + value = value.get() + + # NOTE: We can't call value.build() here: it would likely lead to + # recursion. + pairs.append((name, value)) + return pairs + +# To avoid recursion, this function shouldn't call `value.build()`. +# Recall that Bazel identifies recursion based on the (line, column) that +# a function (or lambda) is **defined** at -- the closure of variables +# is ignored. Thus, Bazel's recursion detection can be incidentally +# triggered if X.build() calls helper(), which calls Y.build(), which +# then calls helper() again -- helper() is indirectly recursive. +def _common_to_kwargs_nobuilders(self, kwargs = None): + if kwargs == None: + kwargs = {} + kwargs.update(self.extra_kwargs) + for name, value in _to_kwargs_get_pairs(kwargs, self): + kwargs[name] = value + + return kwargs + +def _Optional_typedef(): """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. + This allows structs to have attributes whose values can be re-assigned, + e.g. ints, strings, bools, or values where the presence matteres. + """ + +def _Optional_new(*initial): + """Creates an instance. Args: *initial: Either zero, one, or two positional args to set the initial value stored for the optional. - If zero args, then no value is stored. - If one arg, then the arg is the value stored. - If two args, then the first arg is a kwargs dict, and the - second arg is a name in kwargs to look for. If the name is - present in kwargs, it is removed from kwargs and its value - stored, otherwise kwargs is unmodified and no value is stored. + - If zero args, then no value is stored. + - If one arg, then the arg is the value stored. + - If two args, then the first arg is a kwargs dict, and the + second arg is a name in kwargs to look for. If the name is + present in kwargs, it is removed from kwargs and its value + stored, otherwise kwargs is unmodified and no value is stored. Returns: {type}`Optional` @@ -83,14 +161,113 @@ def _Optional_present(self): def _is_optional(obj): return hasattr(obj, "present") -def _RuleCfgBuilder(**kwargs): +Optional = struct( + TYPEDEF = _Optional_typedef, + new = _Optional_new, + get = _Optional_get, + set = _Optional_set, + present = _Optional_present, +) + +def _ExecGroupBuilder_typedef(): + """Builder for {obj}`exec_group()` + + :::{field} toolchains + :type: list[ToolchainTypeBuilder] + ::: + + :::{field} exec_compatible_with + :type: list[str] + ::: + + :::{field} build() -> exec_group + ::: + """ + +def _ExecGroupBuilder_new(**kwargs): + self = struct( + # List of ToolchainTypeBuilders + toolchains = _kwargs_pop_list(kwargs, "toolchains"), + # List of strings + exec_compatible_with = _kwargs_pop_list(kwargs, "exec_compatible_with"), + build = lambda: exec_group(**_common_to_kwargs_nobuilders(self)), + ) + return self + +ExecGroupBuilder = struct( + TYPEDEF = _ExecGroupBuilder_typedef, + new = _ExecGroupBuilder_new, +) + +def _ToolchainTypeBuilder_typedef(): + """Builder for {obj}`config_common.toolchain_type()` + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + + :::{field} mandatory + :type: Optional[bool] + ::: + + :::{field} name + :type: Optional[str | Label] + ::: + + :::{function} build() -> config_common.toolchain_type + ::: + """ + +def _ToolchainTypeBuilder_new(**kwargs): + self = struct( + build = lambda: config_common.toolchain_type(**_common_to_kwargs_nobuilders(self)), + extra_kwargs = kwargs, + mandatory = _Optional_new(kwargs, "mandatory"), + name = _Optional_new(kwargs, "name"), + ) + return self + +ToolchainTypeBuilder = struct( + TYPEDEF = _ToolchainTypeBuilder_typedef, + new = _ToolchainTypeBuilder_new, +) + +def _RuleCfgBuilder_typedef(): + """Wrapper for `rule.cfg` arg. + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + + :::{field} inputs + :type: SetBuilder + ::: + + :::{field} outputs + :type: SetBuilder + ::: + """ + +def _RuleCfgBuilder_new(kwargs): + if kwargs == None: + kwargs = {} + self = struct( _implementation = [kwargs.pop("implementation", None)], - set_implementation = lambda *a, **k: _RuleCfgBuilder_set_implementation(self, *a, **k), + build = lambda: _RuleCfgBuilder_build(self), + extra_kwargs = kwargs, implementation = lambda: _RuleCfgBuilder_implementation(self), - outputs = _SetBuilder(_kwargs_pop_list(kwargs, "outputs")), - inputs = _SetBuilder(_kwargs_pop_list(kwargs, "inputs")), - build = lambda *a, **k: _RuleCfgBuilder_build(self, *a, **k), + # 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(kwargs, "inputs"), + # Bazel requires transition.outputs 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(kwargs, "outputs"), + set_implementation = lambda *a, **k: _RuleCfgBuilder_set_implementation(self, *a, **k), ) return self @@ -112,9 +289,12 @@ def _RuleCfgBuilder_implementation(self): return self._implementation[0] def _RuleCfgBuilder_build(self): - impl = self._implementation[0] + """Builds the rule cfg into the value rule.cfg arg value. - # todo: move these strings into an enum + Returns: + {type}`transition` the transition object to apply to the rule. + """ + impl = self._implementation[0] if impl == "target" or impl == None: return config.target() elif impl == "none": @@ -128,28 +308,86 @@ def _RuleCfgBuilder_build(self): else: return impl -def _RuleBuilder(implementation = None, **kwargs): +RuleCfgBuilder = struct( + TYPEDEF = _RuleCfgBuilder_typedef, + new = _RuleCfgBuilder_new, + implementation = _RuleCfgBuilder_implementation, + set_implementation = _RuleCfgBuilder_set_implementation, + build = _RuleCfgBuilder_build, +) + +def _RuleBuilder_typedef(): + """A builder to accumulate state for constructing a `rule` object. + + + :::{field} attrs + :type: AttrsDict + ::: + + :::{field} cfg + :type: RuleCfgBuilder + ::: + + :::{field} exec_groups + :type: dict[str, ExecGroupBuilder] + ::: + + :::{field} executable + :type: Optional[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + + Additional keyword arguments to use when constructing the rule. Their + values have precedence when creating the rule kwargs. + ::: + + :::{field} fragments + :type: list[str] + ::: + + :::{field} implementation + :type: Optional[callable] + ::: + + :::{field} provides + :type: list[Provider | list[Provider]] + ::: + + :::{field} test + :type: Optional[bool] + ::: + + :::{field} toolchains + :type: list[ToolchainTypeBuilder] + ::: + """ + +def _RuleBuilder_new(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. + **kwargs: The same as the `rule()` function, but using builders or + dicts to specify sub-objects instead of the immutable Bazel + objects. """ # buildifier: disable=uninitialized self = struct( - attrs = _AttrsDict(kwargs.pop("attrs", None)), - cfg = kwargs.pop("cfg", None) or _RuleCfgBuilder(), - # todo: create ExecGroupBuilder (allows mutation) or ExecGroup (allows introspection) + attrs = _AttrsDict_new(kwargs.pop("attrs", None)), + cfg = _RuleCfgBuilder_new(kwargs.pop("cfg", None)), + # dict[str, ExecGroupBuilder] exec_groups = _kwargs_pop_dict(kwargs, "exec_groups"), - executable = _Optional(kwargs, "executable"), - fragments = list(kwargs.pop("fragments", None) or []), - implementation = _Optional(implementation), + executable = _Optional_new(kwargs, "executable"), + # list[str] + fragments = _kwargs_pop_list(kwargs, "fragments"), + implementation = _Optional_new(implementation), extra_kwargs = kwargs, provides = _kwargs_pop_list(kwargs, "provides"), - test = _Optional(kwargs, "test"), - # todo: create ToolchainTypeBuilder or ToolchainType + test = _Optional_new(kwargs, "test"), + # list[ToolchainTypeBuilder] toolchains = _kwargs_pop_list(kwargs, "toolchains"), build = lambda *a, **k: _RuleBuilder_build(self, *a, **k), to_kwargs = lambda *a, **k: _RuleBuilder_to_kwargs(self, *a, **k), @@ -174,35 +412,6 @@ def _RuleBuilder_build(self, debug = ""): print("\n".join(lines)) # buildifier: disable=print return rule(**kwargs) -def _Builder_get_pairs(kwargs, obj): - ignore_names = {"extra_kwargs": None} - pairs = [] - for name in dir(obj): - if name in ignore_names or name in kwargs: - continue - value = getattr(obj, name) - if types.is_function(value): - continue # Assume it's a method - if _is_optional(value): - if not value.present(): - continue - else: - value = value.get() - - # NOTE: We can't call value.build() here: it would likely lead to - # recursion. - pairs.append((name, value)) - return pairs - -# This function isn't allowed to call builders to prevent recursion -def _Builder_to_kwargs_nobuilders(self, kwargs = None): - if kwargs == None: - kwargs = {} - kwargs.update(self.extra_kwargs) - for name, value in _Builder_get_pairs(kwargs, self): - kwargs[name] = value - return kwargs - def _RuleBuilder_to_kwargs(self): """Builds the arguments for calling `rule()`. @@ -213,12 +422,38 @@ def _RuleBuilder_to_kwargs(self): {type}`dict` """ kwargs = dict(self.extra_kwargs) - for name, value in _Builder_get_pairs(kwargs, self): + for name, value in _to_kwargs_get_pairs(kwargs, self): value = value.build() if hasattr(value, "build") else value kwargs[name] = value return kwargs -def _AttrsDict(initial): +RuleBuilder = struct( + TYPEDEF = _RuleBuilder_typedef, + new = _RuleBuilder_new, + build = _RuleBuilder_build, + to_kwargs = _RuleBuilder_to_kwargs, +) + +def _AttrsDict_typedef(): + """Builder for the dictionary of rule attributes. + + :::{field} values + :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 `.values.get(...)` + ::: + + :::{function} items() -> list[tuple[str, object]] + Returns a list of key-value tuples. Convenience wrapper for `.values.items()` + ::: + """ + +def _AttrsDict_new(initial): self = struct( values = {}, update = lambda *a, **k: _AttrsDict_update(self, *a, **k), @@ -231,6 +466,15 @@ def _AttrsDict(initial): return self def _AttrsDict_update(self, other): + """Merge `other` into this object. + + Args: + 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): @@ -239,6 +483,11 @@ def _AttrsDict_update(self, other): self.values[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.values.items(): if hasattr(v, "build"): @@ -248,16 +497,26 @@ def _AttrsDict_build(self): attrs[k] = v return attrs -def _SetBuilder(initial = None): +AttrsDict = struct( + TYPEDEF = _AttrsDict_typedef, + new = _AttrsDict_new, + update = _AttrsDict_update, + build = _AttrsDict_build, +) + +def _SetBuilder(kwargs, name): """Builder for list of unique values. Args: + kwargs: {type}`dict[str, Any]` kwargs to search for `name` + name: {type}`str` A key in `kwargs` to initialize the value + to. If present, kwargs will be modified in place. initial: {type}`list | None` The initial values. Returns: {type}`SetBuilder` """ - initial = {} if not initial else {v: None for v in initial} + initial = {v: None for v in _kwargs_pop_list(kwargs, name)} # buildifier: disable=uninitialized self = struct( @@ -289,53 +548,6 @@ def _SetBuilder_update(self, *others): 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` - """ - - # todo: accept string | exec_group | config.name | config.target | - # transition - - # 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 - ) - def _kwargs_pop_dict(kwargs, key): return dict(kwargs.pop(key, None) or {}) @@ -351,20 +563,20 @@ def _BoolAttrBuilder(**kwargs): # buildifier: disable=uninitialized self = struct( - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), + default = _Optional_new(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), extra_kwargs = kwargs, - build = lambda: attr.bool(**_Builder_to_kwargs_nobuilders(self)), + build = lambda: attr.bool(**_common_to_kwargs_nobuilders(self)), ) return self def _IntAttrBuilder(**kwargs): # buildifier: disable=uninitialized self = struct( - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), + default = _Optional_new(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), values = kwargs.get("values") or [], build = lambda *a, **k: _IntAttrBuilder_build(self, *a, **k), extra_kwargs = kwargs, @@ -372,35 +584,82 @@ def _IntAttrBuilder(**kwargs): return self def _IntAttrBuilder_build(self): - kwargs = _Builder_to_kwargs_nobuilders(self) + kwargs = _common_to_kwargs_nobuilders(self) return attr.int(**kwargs) -def _AttrCfgBuilder(**kwargs): - # todo: For attributes, cfg can be: - # string | transition | config.exec(...) | config.target() | config.none() +# called as: +# AttrBuilder(cfg="exec|target") +# AttrBuilder(cfg=dict(implementation=...)) +# AttrBuilder(cfg=dict(exec_group=...)) +def _AttrCfgBuilder(outer_kwargs, name): + """Create a AttrCfgBuilder. + + Args: + outer_kwargs: {type}`dict` the kwargs to look for `name` within. + name: {type}`str` a key to look for in `outer_kwargs` for the + values to initilize from. If present in `outer_kwargs`, it + will be removed and the value initializes the builder. + + Returns: + {type}`AttrCfgBuilder` + """ + cfg = outer_kwargs.pop(name, None) + if cfg == None: + kwargs = {} + elif types.is_string(cfg): + kwargs = {"cfg": cfg} + else: + # Assume its a dict + kwargs = cfg + + if "cfg" in kwargs: + initial = kwargs.pop("cfg") + is_exec_group = False + elif "exec_group" in kwargs: + initial = kwargs.pop("exec_group") + is_exec_group = True + else: + initial = None + is_exec_group = False + self = struct( - _implementation = [None], + # tuple of (value, bool is_exec_group) + _implementation = [initial, is_exec_group], implementation = lambda: self._implementation[0], set_implementation = lambda *a, **k: _AttrCfgBuilder_set_implementation(self, *a, **k), - outputs = _SetBuilder(_kwargs_pop_list(kwargs, "outputs")), - inputs = _SetBuilder(_kwargs_pop_list(kwargs, "inputs")), + set_exec_group = lambda *a, **k: _AttrCfgBuilder_set_exec_group(self, *a, **k), + exec_group = lambda: _AttrCfgBuilder_exec_group(self), + outputs = _SetBuilder(kwargs, "outputs"), + inputs = _SetBuilder(kwargs, "inputs"), build = lambda: _AttrCfgBuilder_build(self), + extra_kwargs = kwargs, ) return self def _AttrCfgBuilder_set_implementation(self, value): self._implementation[0] = value + self._implementation[1] = False + +def _AttrCfgBuilder_set_exec_group(self, exec_group): + self._implementation[0] = exec_group + self._implementation[1] = True + +def _AttrCfgBuilder_exec_group(self): + if self._implementation[1]: + return self._implementation[0] + else: + return None def _AttrCfgBuilder_build(self): - impl = self._implementation[0] + impl, is_exec_group = self._implementation if impl == None: return None + elif is_exec_group: + return config.exec(impl) elif impl == "target": return config.target() elif impl == "exec": - return config.exec() - elif impl == "???": - return config.exec(impl) + return config.exec() # todo: replace with set_exec(exec_group) elif types.is_function(impl): return transition( implementation = impl, @@ -410,21 +669,42 @@ def _AttrCfgBuilder_build(self): else: return impl -def _LabelAttrBuilder(**kwargs): +def _LabelAttrBuilder_typedef(): + """Builder for `attr.label` objects. + + :::{field} default + :type: Any | configuration_field + ::: + + :::{field} doc + :type: str + ::: + """ + +def _LabelAttrBuilder_new(**kwargs): + """Creates an instance. + + Args: + **kwargs: The same as `attr.label()`. + + Returns: + {type}`LabelAttrBuilder` + """ + # buildifier: disable=uninitialized self = struct( # value or configuration_field - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), - executable = _Optional(kwargs, "executable"), + default = _Optional_new(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), + executable = _Optional_new(kwargs, "executable"), # True, False, or list - allow_files = _Optional(kwargs, "allow_files"), - allow_single_file = _Optional(kwargs, "allow_single_file"), - providers = kwargs.pop("providers", None) or [], - cfg = _Optional(kwargs, "cfg"), - aspects = kwargs.pop("aspects", None) or [], - build = lambda *a, **k: _LabelAttrBuilder_build(self, *a, **k), + allow_files = _Optional_new(kwargs, "allow_files"), + allow_single_file = _Optional_new(kwargs, "allow_single_file"), + providers = _kwargs_pop_list(kwargs, "providers"), + cfg = _AttrCfgBuilder(kwargs, "cfg"), + aspects = _kwargs_pop_list(kwargs, "aspects"), + build = lambda: _LabelAttrBuilder_build(self), extra_kwargs = kwargs, ) return self @@ -433,50 +713,64 @@ def _LabelAttrBuilder_build(self): kwargs = { "aspects": [v.build() for v in self.aspects], } - _Builder_to_kwargs_nobuilders(self, kwargs) + _common_to_kwargs_nobuilders(self, kwargs) for name, value in kwargs.items(): kwargs[name] = value.build() if hasattr(value, "build") else value return attr.label(**kwargs) +LabelAttrBuilder = struct( + TYPEDEF = _LabelAttrBuilder_typedef, + new = _LabelAttrBuilder_new, +) + def _LabelListAttrBuilder(**kwargs): self = struct( - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), - executable = _Optional(kwargs, "executable"), - allow_empty = _Optional(kwargs, "allow_empty"), + default = _kwargs_pop_list(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), + # True, False, or not set + executable = _Optional_new(kwargs, "executable"), + # True or False + allow_empty = _Optional_new(kwargs, "allow_empty"), # True, False, or list - allow_files = _Optional(kwargs, "allow_files"), - providers = kwargs.pop("providers", None) or [], + allow_files = _Optional_new(kwargs, "allow_files"), + providers = _kwargs_pop_list(kwargs, "providers"), # string, config.exec_group, config.none, config.target, or transition # For the latter, it's a builder - cfg = _Optional(kwargs, "cfg"), - aspects = kwargs.pop("aspects", None) or [], - build = lambda *a, **k: attr.label_list(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + cfg = _AttrCfgBuilder(kwargs, "cfg"), + # list of aspect functions + aspects = _kwargs_pop_list(kwargs, "aspects"), + build = lambda: _LabelListAttrBuilder_build(self), extra_kwargs = kwargs, ) return self +def _LabelListAttrBuilder_build(self): + kwargs = _common_to_kwargs_nobuilders(self) + for key, value in kwargs.items(): + kwargs[key] = value.build() if hasattr(value, "build") else value + return attr.label_list(**kwargs) + def _StringListAttrBuilder(**kwargs): self = struct( - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), - allow_empty = _Optional(kwargs, "allow_empty"), + default = _Optional_new(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), + allow_empty = _Optional_new(kwargs, "allow_empty"), # True, False, or list - build = lambda *a, **k: attr.string_list(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + build = lambda *a, **k: attr.string_list(**_common_to_kwargs_nobuilders(self, *a, **k)), extra_kwargs = kwargs, ) return self def _StringAttrBuilder(**kwargs): self = struct( - default = _Optional(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), + default = _Optional_new(kwargs, "default"), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), # True, False, or list - allow_empty = _Optional(kwargs, "allow_empty"), - build = lambda *a, **k: attr.string(**_Builder_to_kwargs_nobuilders(self, *a, **k)), + allow_empty = _Optional_new(kwargs, "allow_empty"), + build = lambda *a, **k: attr.string(**_common_to_kwargs_nobuilders(self, *a, **k)), extra_kwargs = kwargs, values = kwargs.get("values") or [], ) @@ -485,53 +779,25 @@ def _StringAttrBuilder(**kwargs): def _StringDictAttrBuilder(**kwargs): self = struct( default = _kwargs_pop_dict(kwargs, "default"), - doc = _Optional(kwargs, "doc"), - mandatory = _Optional(kwargs, "mandatory"), - allow_empty = _Optional(kwargs, "allow_empty"), - build = lambda: attr.string_dict(**_Builder_to_kwargs_nobuilders(self)), + doc = _Optional_new(kwargs, "doc"), + mandatory = _Optional_new(kwargs, "mandatory"), + allow_empty = _Optional_new(kwargs, "allow_empty"), + build = lambda: attr.string_dict(**_common_to_kwargs_nobuilders(self)), extra_kwargs = kwargs, ) return self -def _Buildable(builder_factory, kwargs_fn = None, ATTR = None, ABSTRACT = False): - if kwargs_fn: - kwargs = kwargs_fn() - built = builder_factory(**kwargs_fn()) - to_builder = struct(build_kwargs = builder_factory, kwargs_fn = kwargs_fn) - else: - to_builder = builder_factory - if not ABSTRACT: - builder = builder_factory() - if hasattr(builder, "build"): - built = builder.build() - elif types.is_dict(builder) and "@build" in builder: - built = builder["@build"](**{k: v for k, v in builder.items() if k != "@build"}) - elif hasattr(builder, "build_kwargs"): - built = builder.build_kwargs(**builder.kwargs_fn()) - else: - fail("bad builder factory:", builder_factory, "->", builder) - if ABSTRACT: - return struct( - build = to_builder().build, # might be recursive issue? - to_builder = to_builder, - ) - return struct( - built = built, - to_builder = to_builder, - ) - rule_builders = struct( - RuleBuilder = _RuleBuilder, - TransitionBuilder = _TransitionBuilder, - SetBuilder = _SetBuilder, - Optional = _Optional, - LabelAttrBuilder = _LabelAttrBuilder, + RuleBuilder = _RuleBuilder_new, + ##SetBuilder = _SetBuilder, + ##Optional = _Optional, + LabelAttrBuilder = _LabelAttrBuilder_new, LabelListAttrBuilder = _LabelListAttrBuilder, - Buildable = _Buildable, IntAttrBuilder = _IntAttrBuilder, StringListAttrBuilder = _StringListAttrBuilder, StringAttrBuilder = _StringAttrBuilder, StringDictAttrBuilder = _StringDictAttrBuilder, BoolAttrBuilder = _BoolAttrBuilder, - RuleCfgBuilder = _RuleCfgBuilder, + ##RuleCfgBuilder = _RuleCfgBuilder, + AttrCfgBuilder = _AttrCfgBuilder, ) From 73c0a55a05524607e3e0e2ea14078be2977f78e0 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 27 Feb 2025 16:03:51 -0800 Subject: [PATCH 05/13] more cleanup --- python/private/rule_builders.bzl | 327 ++++++++++++++++++++++++++----- 1 file changed, 277 insertions(+), 50 deletions(-) diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index cc7e2e3ca4..4363bc305a 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -180,7 +180,7 @@ def _ExecGroupBuilder_typedef(): :type: list[str] ::: - :::{field} build() -> exec_group + :::{function} build() -> exec_group ::: """ @@ -319,7 +319,6 @@ RuleCfgBuilder = struct( def _RuleBuilder_typedef(): """A builder to accumulate state for constructing a `rule` object. - :::{field} attrs :type: AttrsDict ::: @@ -378,16 +377,13 @@ def _RuleBuilder_new(implementation = None, **kwargs): self = struct( attrs = _AttrsDict_new(kwargs.pop("attrs", None)), cfg = _RuleCfgBuilder_new(kwargs.pop("cfg", None)), - # dict[str, ExecGroupBuilder] exec_groups = _kwargs_pop_dict(kwargs, "exec_groups"), executable = _Optional_new(kwargs, "executable"), - # list[str] fragments = _kwargs_pop_list(kwargs, "fragments"), implementation = _Optional_new(implementation), extra_kwargs = kwargs, provides = _kwargs_pop_list(kwargs, "provides"), test = _Optional_new(kwargs, "test"), - # list[ToolchainTypeBuilder] toolchains = _kwargs_pop_list(kwargs, "toolchains"), build = lambda *a, **k: _RuleBuilder_build(self, *a, **k), to_kwargs = lambda *a, **k: _RuleBuilder_to_kwargs(self, *a, **k), @@ -415,6 +411,9 @@ def _RuleBuilder_build(self, debug = ""): def _RuleBuilder_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 @@ -587,18 +586,41 @@ def _IntAttrBuilder_build(self): kwargs = _common_to_kwargs_nobuilders(self) return attr.int(**kwargs) -# called as: -# AttrBuilder(cfg="exec|target") -# AttrBuilder(cfg=dict(implementation=...)) -# AttrBuilder(cfg=dict(exec_group=...)) -def _AttrCfgBuilder(outer_kwargs, name): - """Create a AttrCfgBuilder. +def _AttrCfgBuilder_typedef(): + """Builder for `cfg` arg of label attributes. + + :::{function} implementation() -> callable + + Returns the implementation function for using custom transition. + ::: + + :::{field} outputs + :type: SetBuilder[str | Label] + ::: + + :::{field} inputs + :type: SetBuilder[str | Label] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _AttrCfgBuilder_new(outer_kwargs, name): + """Creates an instance. Args: outer_kwargs: {type}`dict` the kwargs to look for `name` within. name: {type}`str` a key to look for in `outer_kwargs` for the values to initilize from. If present in `outer_kwargs`, it - will be removed and the value initializes the builder. + will be removed and the value initializes the builder. The value + is allowed to be one of: + - The string `exec` or `target` + - A dict with key `implementation`, which is a transition + implementation function. + - A dict with key `exec_group`, which is a string name for an + exec group to use for an exec transition. Returns: {type}`AttrCfgBuilder` @@ -614,20 +636,21 @@ def _AttrCfgBuilder(outer_kwargs, name): if "cfg" in kwargs: initial = kwargs.pop("cfg") - is_exec_group = False + is_exec = False elif "exec_group" in kwargs: initial = kwargs.pop("exec_group") - is_exec_group = True + is_exec = True else: initial = None - is_exec_group = False + is_exec = False self = struct( - # tuple of (value, bool is_exec_group) - _implementation = [initial, is_exec_group], + # list of (value, bool is_exec) + _implementation = [initial, is_exec], implementation = lambda: self._implementation[0], set_implementation = lambda *a, **k: _AttrCfgBuilder_set_implementation(self, *a, **k), - set_exec_group = lambda *a, **k: _AttrCfgBuilder_set_exec_group(self, *a, **k), + set_exec = lambda *a, **k: _AttrCfgBuilder_set_exec(self, *a, **k), + set_target = lambda: _AttrCfgBuilder_set_implementation(self, "target"), exec_group = lambda: _AttrCfgBuilder_exec_group(self), outputs = _SetBuilder(kwargs, "outputs"), inputs = _SetBuilder(kwargs, "inputs"), @@ -636,49 +659,106 @@ def _AttrCfgBuilder(outer_kwargs, name): ) return self -def _AttrCfgBuilder_set_implementation(self, value): - self._implementation[0] = value +def _AttrCfgBuilder_set_implementation(self, impl): + """Sets a custom transition function to use. + + Args: + impl: {type}`callable` a transition implementation function. + """ + self._implementation[0] = impl self._implementation[1] = False -def _AttrCfgBuilder_set_exec_group(self, exec_group): +def _AttrCfgBuilder_set_exec(self, exec_group = None): + """Sets to use an exec transition. + + Args: + exec_group: {type}`str | None` the exec group name to use, if any. + """ self._implementation[0] = exec_group self._implementation[1] = True def _AttrCfgBuilder_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. + + """ if self._implementation[1]: return self._implementation[0] else: return None def _AttrCfgBuilder_build(self): - impl, is_exec_group = self._implementation - if impl == None: + value, is_exec = self._implementation + if value == None: return None - elif is_exec_group: - return config.exec(impl) - elif impl == "target": + elif is_exec: + return config.exec(value) + elif value == "target": return config.target() - elif impl == "exec": - return config.exec() # todo: replace with set_exec(exec_group) - elif types.is_function(impl): + elif types.is_function(value): return transition( - implementation = impl, + implementation = value, inputs = self.inputs.build(), outputs = self.outputs.build(), ) else: return impl +AttrCfgBuilder = struct( + TYPEDEF = _AttrCfgBuilder_typedef, + new = _AttrCfgBuilder_new, + set_implementation = _AttrCfgBuilder_set_implementation, + set_exec = _AttrCfgBuilder_set_exec, + exec_group = _AttrCfgBuilder_exec_group, +) + def _LabelAttrBuilder_typedef(): """Builder for `attr.label` objects. :::{field} default - :type: Any | configuration_field + :type: Optional[str | label | configuration_field | None] ::: :::{field} doc :type: str ::: + + :::{field} mandatory + :type: Optional[bool] + ::: + + :::{field} executable + :type: Optional[bool] + ::: + + :::{field} allow_files + :type: Optional[bool | list[str]] + ::: + + :::{field} allow_single_file + :type: Optional[bool] + ::: + + :::{field} providers + :type: list[provider | list[provider]] + ::: + + :::{field} cfg + :type: AttrCfgBuilder + ::: + + :::{field} aspects + :type: list[aspect] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: """ def _LabelAttrBuilder_new(**kwargs): @@ -693,16 +773,14 @@ def _LabelAttrBuilder_new(**kwargs): # buildifier: disable=uninitialized self = struct( - # value or configuration_field default = _Optional_new(kwargs, "default"), doc = _Optional_new(kwargs, "doc"), mandatory = _Optional_new(kwargs, "mandatory"), executable = _Optional_new(kwargs, "executable"), - # True, False, or list allow_files = _Optional_new(kwargs, "allow_files"), allow_single_file = _Optional_new(kwargs, "allow_single_file"), providers = _kwargs_pop_list(kwargs, "providers"), - cfg = _AttrCfgBuilder(kwargs, "cfg"), + cfg = _AttrCfgBuilder_new(kwargs, "cfg"), aspects = _kwargs_pop_list(kwargs, "aspects"), build = lambda: _LabelAttrBuilder_build(self), extra_kwargs = kwargs, @@ -721,24 +799,63 @@ def _LabelAttrBuilder_build(self): LabelAttrBuilder = struct( TYPEDEF = _LabelAttrBuilder_typedef, new = _LabelAttrBuilder_new, + build = _LabelAttrBuilder_build, ) -def _LabelListAttrBuilder(**kwargs): +def _LabelListAttrBuilder_typedef(): + """Builder for `attr.label_list` + + :::{field} default + :type: Optional[list[str|Label] | configuration_field] + ::: + + :::{field} doc + :type: Optional[str] + ::: + + :::{field} mandatory + :type: Optional[bool] + ::: + + :::{field} executable + :type: Optional[bool] + ::: + + :::{field} allow_files + :type: Optional[bool | list[str]] + ::: + + :::{field} allow_empty + :type: Optional[bool] + ::: + + :::{field} providers + :type: list[provider | list[provider]] + ::: + + :::{field} cfg + :type: AttrCfgBuilder + ::: + + :::{field} aspects + :type: list[aspect] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _LabelListAttrBuilder_new(**kwargs): self = struct( default = _kwargs_pop_list(kwargs, "default"), doc = _Optional_new(kwargs, "doc"), mandatory = _Optional_new(kwargs, "mandatory"), - # True, False, or not set executable = _Optional_new(kwargs, "executable"), - # True or False allow_empty = _Optional_new(kwargs, "allow_empty"), - # True, False, or list allow_files = _Optional_new(kwargs, "allow_files"), providers = _kwargs_pop_list(kwargs, "providers"), - # string, config.exec_group, config.none, config.target, or transition - # For the latter, it's a builder - cfg = _AttrCfgBuilder(kwargs, "cfg"), - # list of aspect functions + cfg = _AttrCfgBuilder_new(kwargs, "cfg"), aspects = _kwargs_pop_list(kwargs, "aspects"), build = lambda: _LabelListAttrBuilder_build(self), extra_kwargs = kwargs, @@ -751,19 +868,86 @@ def _LabelListAttrBuilder_build(self): kwargs[key] = value.build() if hasattr(value, "build") else value return attr.label_list(**kwargs) -def _StringListAttrBuilder(**kwargs): +LabelListAttrBuilder = struct( + TYPEDEF = _LabelListAttrBuilder_typedef, + new = _LabelListAttrBuilder_new, + build = _LabelListAttrBuilder_build, +) + +def _StringListAttrBuilder_typedef(): + """Builder for `attr.string_list` + + :::{field} default + :type: Optiona[list[str] | configuration_field] + ::: + + :::{field} doc + :type: Optional[str] + ::: + + :::{field} mandatory + :type: Optional[bool] + ::: + + :::{field} allow_empty + :type: Optional[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{function} build() -> attr.string_list + ::: + """ + +def _StringListAttrBuilder_new(**kwargs): self = struct( default = _Optional_new(kwargs, "default"), doc = _Optional_new(kwargs, "doc"), mandatory = _Optional_new(kwargs, "mandatory"), allow_empty = _Optional_new(kwargs, "allow_empty"), - # True, False, or list build = lambda *a, **k: attr.string_list(**_common_to_kwargs_nobuilders(self, *a, **k)), extra_kwargs = kwargs, ) return self -def _StringAttrBuilder(**kwargs): +StringListBuilder = struct( + TYPEDEF = _StringListBuilder_typedef, + new = _StringListBuilder_new, +) + +def _StringAttrBuilder_typedef(): + """Builder for `attr.string` + + :::{field} default + :type: Optiona[str] + ::: + + :::{field} doc + :type: Optiona[str] + ::: + :::{field} mandatory + :type: Optiona[bool] + ::: + + :::{field} allow_empty + :type: Optiona[bool] + ::: + + :::{function} build() -> attr.string + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{field} values + :type: list[str] + ::: + """ + +def _StringAttrBuilder_new(**kwargs): self = struct( default = _Optional_new(kwargs, "default"), doc = _Optional_new(kwargs, "doc"), @@ -772,11 +956,51 @@ def _StringAttrBuilder(**kwargs): allow_empty = _Optional_new(kwargs, "allow_empty"), build = lambda *a, **k: attr.string(**_common_to_kwargs_nobuilders(self, *a, **k)), extra_kwargs = kwargs, - values = kwargs.get("values") or [], + values = _kwargs_pop_list(kwargs, "values"), ) return self -def _StringDictAttrBuilder(**kwargs): +StringAttrBuilder = struct( + TYPEDEF = _StringAttrBuilder_typedef, + new = _StringAttrBuilder_new, +) + +def _StringDictAttrBuilder_typedef(): + """Builder for `attr.string_dict` + + :::{field} default + :type: dict[str, str], + ::: + + :::{field} doc + :type: Optional[str] + ::: + + :::{field} mandatory + :type: Optional[bool] + ::: + + :::{field} allow_empty + :type: Optional[bool] + ::: + + :::{function} build() -> attr.string_dict + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _StringDictAttrBuilder_new(**kwargs): + """Creates an instance. + + Args: + **kwargs: {type}`dict` The same args as for `attr.string_dict`. + + Returns: + {type}`StringDictAttrBuilder` + """ self = struct( default = _kwargs_pop_dict(kwargs, "default"), doc = _Optional_new(kwargs, "doc"), @@ -787,10 +1011,14 @@ def _StringDictAttrBuilder(**kwargs): ) return self +StringDictAttrBuilder = struct( + TYPEDEF = _StringDictAttrBuilder_typedef, + new = _StringDictAttrBuilder_new, +) + +# todo: remove Builder suffixes on all these? rule_builders = struct( RuleBuilder = _RuleBuilder_new, - ##SetBuilder = _SetBuilder, - ##Optional = _Optional, LabelAttrBuilder = _LabelAttrBuilder_new, LabelListAttrBuilder = _LabelListAttrBuilder, IntAttrBuilder = _IntAttrBuilder, @@ -798,6 +1026,5 @@ rule_builders = struct( StringAttrBuilder = _StringAttrBuilder, StringDictAttrBuilder = _StringDictAttrBuilder, BoolAttrBuilder = _BoolAttrBuilder, - ##RuleCfgBuilder = _RuleCfgBuilder, AttrCfgBuilder = _AttrCfgBuilder, ) From b9902c870fa11a68a94693cdb5100f0263e138b5 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 28 Feb 2025 23:26:22 -0800 Subject: [PATCH 06/13] cleanup --- docs/BUILD.bazel | 2 + python/private/BUILD.bazel | 19 + python/private/attr_builders.bzl | 932 +++++++++++++++++++++ python/private/attributes.bzl | 59 +- python/private/builders_util.bzl | 320 +++++++ python/private/py_binary_rule.bzl | 1 - python/private/py_executable.bzl | 57 +- python/private/py_library.bzl | 26 +- python/private/rule_builders.bzl | 860 ++++--------------- sphinxdocs/inventories/bazel_inventory.txt | 1 + tests/scratch/defs1.bzl | 2 - tests/support/sh_py_run_test.bzl | 11 +- 12 files changed, 1496 insertions(+), 794 deletions(-) create mode 100644 python/private/attr_builders.bzl create mode 100644 python/private/builders_util.bzl diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index cb49310418..19bf801091 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -97,6 +97,8 @@ sphinx_stardocs( "//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", diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 3e24014a6d..0047dafad5 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -51,10 +51,20 @@ filegroup( visibility = ["//python:__pkg__"], ) +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", @@ -92,11 +102,20 @@ bzl_library( ], ) +bzl_library( + name = "builders_util_bzl", + srcs = ["builders_util.bzl"], + deps = [ + "@bazel_skylib//lib:types", + ], +) + bzl_library( name = "rule_builders_bzl", srcs = ["rule_builders.bzl"], deps = [ ":builders_bzl", + ":builders_util_bzl", "@bazel_skylib//lib:types", ], ) diff --git a/python/private/attr_builders.bzl b/python/private/attr_builders.bzl new file mode 100644 index 0000000000..93c3a2f5f9 --- /dev/null +++ b/python/private/attr_builders.bzl @@ -0,0 +1,932 @@ +# 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", + "Optional", + "UniqueList", + "Value", + "common_to_kwargs_nobuilders", + "kwargs_pop_dict", + "kwargs_pop_doc", + "kwargs_pop_list", + "kwargs_pop_mandatory", +) + +def _kwargs_pop_allow_empty(kwargs): + return Value.kwargs(kwargs, "allow_empty", True) + +def _AttrCfg_typedef(): + """Builder for `cfg` arg of label attributes. + + :::{function} implementation() -> callable | None + + Returns the implementation function when a custom transition is being used. + ::: + + :::{field} outputs + :type: UniqueList[Label] + ::: + + :::{field} inputs + :type: UniqueList[str | Label] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _AttrCfg_new(outer_kwargs, name): + """Creates a builder for the `attr.cfg` attribute. + + Args: + outer_kwargs: {type}`dict` the kwargs to look for `name` within. + name: {type}`str` a key to look for in `outer_kwargs` for the + values to initilize from. If present in `outer_kwargs`, it + will be removed and the value initializes the builder. The value + is allowed to be one of: + - The string `exec` or `target` + - A dict with key `implementation`, which is a transition + implementation function. + - A dict with key `exec_group`, which is a string name for an + exec group to use for an exec transition. + + Returns: + {type}`AttrCfg` + """ + cfg = outer_kwargs.pop(name, None) + if cfg == None: + kwargs = {} + elif types.is_string(cfg): + kwargs = {"cfg": cfg} + else: + # Assume its a dict + kwargs = cfg + + if "cfg" in kwargs: + initial = kwargs.pop("cfg") + is_exec = False + elif "exec_group" in kwargs: + initial = kwargs.pop("exec_group") + is_exec = True + else: + initial = None + is_exec = False + + # buildifier: disable=uninitialized + self = struct( + # list of (value, bool is_exec) + _implementation = [initial, is_exec], + implementation = lambda: self._implementation[0], + set_implementation = lambda *a, **k: _AttrCfg_set_implementation(self, *a, **k), + set_exec = lambda *a, **k: _AttrCfg_set_exec(self, *a, **k), + set_target = lambda: _AttrCfg_set_implementation(self, "target"), + exec_group = lambda: _AttrCfg_exec_group(self), + outputs = UniqueList.new(kwargs, "outputs"), + inputs = UniqueList.new(kwargs, "inputs"), + build = lambda: _AttrCfg_build(self), + extra_kwargs = kwargs, + ) + return self + +def _AttrCfg_set_implementation(self, impl): + """Sets a custom transition function to use. + + Args: + impl: {type}`callable` a transition implementation function. + """ + self._implementation[0] = impl + self._implementation[1] = False + +def _AttrCfg_set_exec(self, exec_group = None): + """Sets to use an exec transition. + + Args: + exec_group: {type}`str | None` the exec group name to use, if any. + """ + self._implementation[0] = exec_group + self._implementation[1] = True + +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. + + """ + if self._implementation[1]: + return self._implementation[0] + else: + return None + +def _AttrCfg_build(self): + value, is_exec = self._implementation + if value == None: + return None + elif is_exec: + return config.exec(value) + elif value == "target": + return config.target() + elif types.is_function(value): + return transition( + implementation = value, + inputs = self.inputs.build(), + outputs = self.outputs.build(), + ) + else: + # Otherwise, just assume the value is valid and whoever set it + # knows what they're doing. + return value + +AttrCfg = struct( + TYPEDEF = _AttrCfg_typedef, + new = _AttrCfg_new, + set_implementation = _AttrCfg_set_implementation, + set_exec = _AttrCfg_set_exec, + exec_group = _AttrCfg_exec_group, +) + +def _Bool_typedef(): + """Builder fo attr.bool. + + :::{function} build() -> attr.bool + ::: + + :::{field} default + :type: Value[bool] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + """ + +def _Bool_new(**kwargs): + """Creates a builder for `attr.bool`. + + Args: + **kwargs: Same kwargs as {obj}`attr.bool` + + Returns: + {type}`Bool` + """ + + # buildifier: disable=uninitialized + self = struct( + default = Value.kwargs(kwargs, "default", False), + doc = kwargs_pop_doc(kwargs), + mandatory = kwargs_pop_mandatory(kwargs), + extra_kwargs = kwargs, + build = lambda: attr.bool(**common_to_kwargs_nobuilders(self)), + ) + return self + +Bool = struct( + TYPEDEF = _Bool_typedef, + new = _Bool_new, +) + +def _Int_typedef(): + """Builder for attr.int. + + :::{function} build() -> attr.int + ::: + + :::{field} default + :type: Value[int] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} values + :type: list[int] + ::: + """ + +def _Int_new(**kwargs): + """Creates a builder for `attr.int`. + + Args: + **kwargs: Same kwargs as {obj}`attr.int` + + Returns: + {type}`Int` + """ + + # buildifier: disable=uninitialized + self = struct( + build = lambda: attr.int(**common_to_kwargs_nobuilders(self)), + default = Value.kwargs(kwargs, "default", 0), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + values = kwargs_pop_list(kwargs, "values"), + ) + return self + +Int = struct( + TYPEDEF = _Int_typedef, + new = _Int_new, +) + +def _IntList_typedef(): + """Builder for attr.int_list. + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.int_list + ::: + + :::{field} default + :type: list[int] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + """ + +def _IntList_new(**kwargs): + """Creates a builder for `attr.int_list`. + + Args: + **kwargs: Same as {obj}`attr.int_list`. + + Returns: + {type}`IntList` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda: attr.int_list(**common_to_kwargs_nobuilders(self)), + default = kwargs_pop_list(kwargs, "default"), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +IntList = struct( + TYPEDEF = _IntList_typedef, + new = _IntList_new, +) + +def _Label_typedef(): + """Builder for `attr.label` objects. + + :::{field} default + :type: Value[str | label | configuration_field | None] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} executable + :type: Value[bool] + ::: + + :::{field} allow_files + :type: Optional[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. + ::: + + :::{field} allow_single_file + :type: Optional[bool | None] + + Note that `allow_single_file` is mutually exclusive with `allow_files`. + Only one of the two can have a value set. + ::: + + :::{field} providers + :type: list[provider | list[provider]] + ::: + + :::{field} cfg + :type: AttrCfg + ::: + + :::{field} aspects + :type: list[aspect] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _Label_new(**kwargs): + """Creates a builder for `attr.label`. + + Args: + **kwargs: The same as {obj}`attr.label()`. + + Returns: + {type}`Label` + """ + + # buildifier: disable=uninitialized + self = struct( + default = Value.kwargs(kwargs, "default", None), + doc = kwargs_pop_doc(kwargs), + mandatory = kwargs_pop_mandatory(kwargs), + executable = Value.kwargs(kwargs, "executable", False), + allow_files = Optional.new(kwargs, "allow_files"), + allow_single_file = Optional.new(kwargs, "allow_single_file"), + providers = kwargs_pop_list(kwargs, "providers"), + cfg = _AttrCfg_new(kwargs, "cfg"), + aspects = kwargs_pop_list(kwargs, "aspects"), + build = lambda: _Label_build(self), + extra_kwargs = kwargs, + ) + return self + +def _Label_build(self): + kwargs = dict(self.extra_kwargs) + if "aspects" not in kwargs: + kwargs["aspects"] = [v.build() for v in self.aspects] + + common_to_kwargs_nobuilders(self, kwargs) + for name, value in kwargs.items(): + kwargs[name] = value.build() if hasattr(value, "build") else value + return attr.label(**kwargs) + +Label = struct( + TYPEDEF = _Label_typedef, + new = _Label_new, + build = _Label_build, +) + +def _LabelKeyedStringDict_typedef(): + """Builder for attr.label_keyed_string_dict. + + :::{field} aspects + :type: list[aspect] + ::: + + :::{field} allow_files + :type: Value[bool | list[str]] + ::: + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{field} cfg + :type: AttrCfg + ::: + + :::{field} default + :type: Value[dict[str|Label, str] | callable] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} providers + :type: list[provider | list[provider]] + ::: + """ + +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` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + allow_files = Value.kwargs(kwargs, "allow_files", False), + aspects = kwargs_pop_list(kwargs, "aspects"), + build = lambda: _LabelList_build(self), + cfg = _AttrCfg_new(kwargs, "cfg"), + default = Value.kwargs(kwargs, "default", {}), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + providers = kwargs_pop_list(kwargs, "providers"), + ) + return self + +LabelKeyedStringDict = struct( + TYPEDEF = _LabelKeyedStringDict_typedef, + new = _LabelKeyedStringDict_new, +) + +def _LabelList_typedef(): + """Builder for `attr.label_list` + + :::{field} aspects + :type: list[aspect] + ::: + + :::{field} allow_files + :type: Value[bool | list[str]] + ::: + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{field} cfg + :type: AttrCfg + ::: + + :::{field} default + :type: Value[list[str|Label] | configuration_field | callable] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} providers + :type: list[provider | list[provider]] + ::: + """ + +def _LabelList_new(**kwargs): + """Creates a builder for `attr.label_list`. + + Args: + **kwargs: Same as {obj}`attr.label_list`. + + Returns: + {type}`LabelList` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + allow_files = Value.kwargs(kwargs, "allow_files", False), + aspects = kwargs_pop_list(kwargs, "aspects"), + build = lambda: _LabelList_build(self), + cfg = _AttrCfg_new(kwargs, "cfg"), + default = Value.kwargs(kwargs, "default", []), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + providers = kwargs_pop_list(kwargs, "providers"), + ) + return self + +def _LabelList_build(self): + """Creates a {obj}`attr.label_list`.""" + + kwargs = common_to_kwargs_nobuilders(self) + for key, value in kwargs.items(): + kwargs[key] = value.build() if hasattr(value, "build") else value + return attr.label_list(**kwargs) + +LabelList = struct( + TYPEDEF = _LabelList_typedef, + new = _LabelList_new, + build = _LabelList_build, +) + +def _Output_typedef(): + """Builder for attr.output + + :::{function} build() -> attr.output + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + """ + +def _Output_new(**kwargs): + """Creates a builder for `attr.output`. + + Args: + **kwargs: Same as {obj}`attr.output`. + + Returns: + {type}`Output` + """ + + # buildifier: disable=uninitialized + self = struct( + build = lambda: attr.output(**common_to_kwargs_nobuilders(self)), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +Output = struct( + TYPEDEF = _Output_typedef, + new = _Output_new, +) + +def _OutputList_typedef(): + """Builder for attr.output_list + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.output + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, object] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + """ + +def _OutputList_new(**kwargs): + """Creates a builder for `attr.output_list`. + + Args: + **kwargs: Same as {obj}`attr.output_list`. + + Returns: + {type}`OutputList` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda: attr.output_list(**common_to_kwargs_nobuilders(self)), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +OutputList = struct( + TYPEDEF = _OutputList_typedef, + new = _OutputList_new, +) + +def _String_typedef(): + """Builder for `attr.string` + + :::{function} build() -> attr.string + ::: + + :::{field} default + :type: Value[str | configuration_field] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} values + :type: list[str] + ::: + """ + +def _String_new(**kwargs): + """Creates a builder for `attr.string`. + + Args: + **kwargs: Same as {obj}`attr.string`. + + Returns: + {type}`String` + """ + + # buildifier: disable=uninitialized + self = struct( + default = Value.kwargs(kwargs, "default", ""), + doc = kwargs_pop_doc(kwargs), + mandatory = kwargs_pop_mandatory(kwargs), + build = lambda *a, **k: attr.string(**common_to_kwargs_nobuilders(self, *a, **k)), + extra_kwargs = kwargs, + values = kwargs_pop_list(kwargs, "values"), + ) + return self + +String = struct( + TYPEDEF = _String_typedef, + new = _String_new, +) + +def _StringDict_typedef(): + """Builder for `attr.string_dict` + + :::{field} default + :type: dict[str, str] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.string_dict + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _StringDict_new(**kwargs): + """Creates a builder for `attr.string_dict`. + + Args: + **kwargs: The same args as for `attr.string_dict`. + + Returns: + {type}`StringDict` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda: attr.string_dict(**common_to_kwargs_nobuilders(self)), + default = kwargs_pop_dict(kwargs, "default"), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +StringDict = struct( + TYPEDEF = _StringDict_typedef, + new = _StringDict_new, +) + +def _StringKeyedLabelDict_typedef(): + """Builder for attr.string_keyed_label_dict. + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.string_list + ::: + + :::{field} default + :type: dict[str, Label] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +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` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda *a, **k: attr.string_list(**common_to_kwargs_nobuilders(self, *a, **k)), + default = kwargs_pop_dict(kwargs, "default"), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +StringKeyedLabelDict = struct( + TYPEDEF = _StringKeyedLabelDict_typedef, + new = _StringKeyedLabelDict_new, +) + +def _StringList_typedef(): + """Builder for `attr.string_list` + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.string_list + ::: + + :::{field} default + :type: Value[list[str] | configuration_field] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _StringList_new(**kwargs): + """Creates a builder for `attr.string_list`. + + Args: + **kwargs: Same as {obj}`attr.string_list`. + + Returns: + {type}`StringList` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda: attr.string_list(**common_to_kwargs_nobuilders(self)), + default = Value.kwargs(kwargs, "default", []), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +StringList = struct( + TYPEDEF = _StringList_typedef, + new = _StringList_new, +) + +def _StringListDict_typedef(): + """Builder for attr.string_list_dict. + + :::{field} allow_empty + :type: Value[bool] + ::: + + :::{function} build() -> attr.string_list + ::: + + :::{field} default + :type: dict[str, list[str]] + ::: + + :::{field} doc + :type: Value[str] + ::: + + :::{field} mandatory + :type: Value[bool] + ::: + + :::{field} extra_kwargs + :type: dict[str, Any] + ::: + """ + +def _StringListDict_new(**kwargs): + """Creates a builder for `attr.string_list_dict`. + + Args: + **kwargs: Same as {obj}`attr.string_list_dict`. + + Returns: + {type}`StringListDict` + """ + + # buildifier: disable=uninitialized + self = struct( + allow_empty = _kwargs_pop_allow_empty(kwargs), + build = lambda: attr.string_list(**common_to_kwargs_nobuilders(self)), + default = kwargs_pop_dict(kwargs, "default"), + doc = kwargs_pop_doc(kwargs), + extra_kwargs = kwargs, + mandatory = kwargs_pop_mandatory(kwargs), + ) + return self + +StringListDict = struct( + TYPEDEF = _StringListDict_typedef, + new = _StringListDict_new, +) + +attrb = struct( + 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, +) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index dfec41a43e..b96ac365b7 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -16,14 +16,13 @@ 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(":builders.bzl", "builders") -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", "rule_builders") +load(":rule_builders.bzl", "ruleb") load( ":semantics.bzl", "DEPS_ATTR_ALLOW_RULES", @@ -44,12 +43,18 @@ _PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", Non # 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] @@ -144,7 +149,7 @@ PycCollectionAttr = enum( def create_stamp_attr(**kwargs): return { - "stamp": lambda: rule_builders.IntAttrBuilder( + "stamp": lambda: attrb.Int( values = _STAMP_VALUES, doc = """ Whether to encode build information into the binary. Possible values: @@ -205,7 +210,7 @@ CC_TOOLCHAIN = { DATA_ATTRS = { # NOTE: The "flags" attribute is deprecated, but there isn't an alternative # way to specify that constraints should be ignored. - "data": lambda: rule_builders.LabelListAttrBuilder( + "data": lambda: attrb.LabelList( allow_files = True, flags = ["SKIP_CONSTRAINTS_OVERRIDE"], doc = """ @@ -233,7 +238,7 @@ def _create_native_rules_allowlist_attrs(): providers = [] return { - "_native_rules_allowlist": lambda: rule_builders.LabelAttrBuilder( + "_native_rules_allowlist": lambda: attrb.Label( default = default, providers = providers, ), @@ -259,7 +264,7 @@ COMMON_ATTRS = dicts.add( ) IMPORTS_ATTRS = { - "imports": lambda: rule_builders.StringListAttrBuilder( + "imports": lambda: attrb.StringList( doc = """ List of import directories to be added to the PYTHONPATH. @@ -279,7 +284,7 @@ _MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else [] # Attributes common to rules accepting Python sources and deps. PY_SRCS_ATTRS = dicts.add( { - "deps": lambda: rule_builders.LabelListAttrBuilder( + "deps": lambda: attrb.LabelList( providers = [ [PyInfo], [CcInfo], @@ -298,7 +303,7 @@ Targets that only provide data files used at runtime belong in the `data` attribute. """, ), - "precompile": lambda: rule_builders.StringAttrBuilder( + "precompile": lambda: attrb.String( doc = """ Whether py source files **for this target** should be precompiled. @@ -320,7 +325,7 @@ Values: default = PrecompileAttr.INHERIT, values = sorted(PrecompileAttr.__members__.values()), ), - "precompile_invalidation_mode": lambda: rule_builders.StringAttrBuilder( + "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: @@ -338,7 +343,7 @@ https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode default = PrecompileInvalidationModeAttr.AUTO, values = sorted(PrecompileInvalidationModeAttr.__members__.values()), ), - "precompile_optimize_level": lambda: rule_builders.IntAttrBuilder( + "precompile_optimize_level": lambda: attrb.Int( doc = """ The optimization level for precompiled files. @@ -351,7 +356,7 @@ runtime when the code actually runs. """, default = 0, ), - "precompile_source_retention": lambda: rule_builders.StringAttrBuilder( + "precompile_source_retention": lambda: attrb.String( default = PrecompileSourceRetentionAttr.INHERIT, values = sorted(PrecompileSourceRetentionAttr.__members__.values()), doc = """ @@ -363,7 +368,7 @@ in the resulting output or not. Valid values are: * `omit_source`: Don't include the original py source. """, ), - "pyi_deps": lambda: rule_builders.LabelListAttrBuilder( + "pyi_deps": lambda: attrb.LabelList( doc = """ Dependencies providing type definitions the library needs. @@ -379,7 +384,7 @@ program (packaging rules may include them, however). [CcInfo], ] + _MaybeBuiltinPyInfo, ), - "pyi_srcs": lambda: rule_builders.LabelListAttrBuilder( + "pyi_srcs": lambda: attrb.LabelList( doc = """ Type definition files for the library. @@ -394,7 +399,7 @@ as part of a runnable program (packaging rules may include them, however). ), # Required attribute, but details vary by rule. # Use create_srcs_attr to create one. - "srcs": lambda: rule_builders.LabelListAttrBuilder( + "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. @@ -410,20 +415,20 @@ files that may be needed at run time belong in `data`. # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute # has a separate story. ##"srcs_version": None, - "srcs_version": lambda: rule_builders.StringAttrBuilder( + "srcs_version": lambda: attrb.String( doc = "Defunct, unused, does nothing.", ), - "_precompile_flag": lambda: rule_builders.LabelAttrBuilder( + "_precompile_flag": lambda: attrb.Label( default = "//python/config_settings:precompile", providers = [BuildSettingInfo], ), - "_precompile_source_retention_flag": lambda: rule_builders.LabelAttrBuilder( + "_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": lambda: rule_builders.BoolAttrBuilder( + "_use_auto_exec_groups": lambda: attrb.Bool( default = True, ), }, @@ -432,14 +437,14 @@ files that may be needed at run time belong in `data`. COVERAGE_ATTRS = { # Magic attribute to help C++ coverage work. There's no # docs about this; see TestActionBuilder.java - "_collect_cc_coverage": lambda: rule_builders.LabelAttrBuilder( + "_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: rule_builders.LabelAttrBuilder( + "_lcov_merger": lambda: attrb.Label( default = configuration_field(fragment = "coverage", name = "output_generator"), executable = True, cfg = "exec", @@ -452,7 +457,7 @@ COVERAGE_ATTRS = { AGNOSTIC_EXECUTABLE_ATTRS = dicts.add( DATA_ATTRS, { - "env": lambda: rule_builders.StringDictAttrBuilder( + "env": lambda: attrb.StringDict( doc = """\ Dictionary of strings; optional; values are subject to `$(location)` and "Make variable" substitution. @@ -475,7 +480,7 @@ AGNOSTIC_TEST_ATTRS = dicts.add( # Tests have stamping disabled by default. create_stamp_attr(default = 0), { - "env_inherit": lambda: rule_builders.StringListAttrBuilder( + "env_inherit": lambda: attrb.StringList( doc = """\ List of strings; optional @@ -484,7 +489,7 @@ environment when the test is executed by bazel test. """, ), # TODO(b/176993122): Remove when Bazel automatically knows to run on darwin. - "_apple_constraints": lambda: rule_builders.LabelListAttrBuilder( + "_apple_constraints": lambda: attrb.LabelList( default = [ "@platforms//os:ios", "@platforms//os:macos", diff --git a/python/private/builders_util.bzl b/python/private/builders_util.bzl new file mode 100644 index 0000000000..3bf92f360f --- /dev/null +++ b/python/private/builders_util.bzl @@ -0,0 +1,320 @@ +# 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. + +load("@bazel_skylib//lib:types.bzl", "types") + +def kwargs_pop_dict(kwargs, key): + """Get a dict value for a kwargs key. + + """ + existing = kwargs.pop(key, None) + if existing == None: + return {} + else: + return { + k: v() if types.is_function(v) else v + for k, v in existing.items() + } + +def kwargs_pop_list(kwargs, key): + existing = kwargs.pop(key, None) + if existing == None: + return [] + else: + return [ + v() if types.is_function(v) else v + for v in existing + ] + +def kwargs_pop_doc(kwargs): + return _Value_kwargs(kwargs, "doc", "") + +def kwargs_pop_mandatory(kwargs): + return _Value_kwargs(kwargs, "mandatory", False) + +def to_kwargs_get_pairs(obj, existing_kwargs): + """Partially converts attributes of `obj` to kwarg values. + + This is not a recursive function. Callers must manually handle: + * Attributes that are lists/dicts of non-primitive values. + * Attributes that are builders. + + Args: + obj: A struct whose attributes to turn into kwarg vales. + existing_kwargs: Existing kwarg values that should are already + computed and this function should ignore. + + Returns: + {type}`list[tuple[str, object | Builder]]` a list of key-value + tuples, where the keys are kwarg names, and the values are + a builder for the final value or the final kwarg value. + """ + ignore_names = {"extra_kwargs": None} + pairs = [] + for name in dir(obj): + if name in ignore_names or name in existing_kwargs: + continue + value = getattr(obj, name) + if types.is_function(value): + continue # Assume it's a method + if _is_value_wrapper(value): + value = value.get() + elif _is_optional(value): + if not value.present(): + continue + else: + value = value.get() + + # NOTE: We can't call value.build() here: it would likely lead to + # recursion. + pairs.append((name, value)) + return pairs + +# To avoid recursion, this function shouldn't call `value.build()`. +# Recall that Bazel identifies recursion based on the (line, column) that +# a function (or lambda) is **defined** at -- the closure of variables +# is ignored. Thus, Bazel's recursion detection can be incidentally +# triggered if X.build() calls helper(), which calls Y.build(), which +# then calls helper() again -- helper() is indirectly recursive. +def common_to_kwargs_nobuilders(self, kwargs = None): + """Convert attributes of `self` to kwargs. + + Args: + self: the object whose attributes to convert. + kwargs: An existing kwargs dict to populate. + + Returns: + {type}`dict[str, object]` A new kwargs dict, or the passed-in `kwargs` + if one was passed in. + """ + if kwargs == None: + kwargs = {} + kwargs.update(self.extra_kwargs) + for name, value in to_kwargs_get_pairs(self, kwargs): + kwargs[name] = value + + return kwargs + +def _Optional_typedef(): + """A wrapper for a re-assignable value that may or may not exist at all. + + This allows structs to have attributes whose values can be re-assigned, + e.g. ints, strings, bools, or values where the presence matters. + + This is like {obj}`Value`, except it supports not having a value specified + at all. This allows entirely omitting an argument when the arguments + are constructed for calling e.g. `rule()` + + :::{function} clear() + ::: + """ + +def _Optional_new(*initial): + """Creates an instance. + + Args: + *initial: Either zero, one, or two positional args to set the + initial value stored for the optional. + - If zero args, then no value is stored. + - If one arg, then the arg is the value stored. + - If two args, then the first arg is a kwargs dict, and the + second arg is a name in kwargs to look for. If the name is + present in kwargs, it is removed from kwargs and its value + stored, otherwise kwargs is unmodified and no value is stored. + + Returns: + {type}`Optional` + """ + if len(initial) > 2: + fail("Only zero, one, or two positional args allowed, but got: {}".format(initial)) + + if len(initial) == 2: + kwargs, name = initial + if name in kwargs: + initial = [kwargs.pop(name)] + else: + initial = [] + else: + initial = list(initial) + + # buildifier: disable=uninitialized + self = struct( + # Length zero when no value; length one when has value. + # NOTE: This name is load bearing: it indicates this is a Value + # object; see _is_optional() + _Optional_value = initial, + present = lambda *a, **k: _Optional_present(self, *a, **k), + clear = lambda: self.Optional_value.clear(), + 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._Optional_value) == 0: + self._Optional_value.append(value) + else: + self._Optional_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._Optional_value): + fail("Value not present") + return self._Optional_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._Optional_value) > 0 + +def _is_optional(obj): + return hasattr(obj, "_Optional_value") + +Optional = struct( + TYPEDEF = _Optional_typedef, + new = _Optional_new, + get = _Optional_get, + set = _Optional_set, + present = _Optional_present, +) + +def _Value_typedef(): + """A wrapper for a re-assignable value that always has some value. + + This allows structs to have attributes whose values can be re-assigned, + e.g. ints, strings, bools, etc. + + This is similar to Optional, except it will always have *some* value + as a default (e.g. None, empty string, empty list, etc) that is OK to pass + onto the rule(), attribute, etc function. + + :::{function} get() -> object + ::: + """ + +def _Value_new(initial): + # buildifier: disable=uninitialized + self = struct( + # NOTE: This name is load bearing: it indicates this is a Value + # object; see _is_value_wrapper() + _Value_value = [initial], + get = lambda: self._Value_value[0], + set = lambda v: _Value_set(self, v), + ) + return self + +def _Value_kwargs(kwargs, name, default): + if name in kwargs: + initial = kwargs[name] + else: + initial = default + return _Value_new(initial) + +def _Value_set(self, v): + """Sets the value. + + Args: + v: the value to set. + """ + self._Value_value[0] = v + +def _is_value_wrapper(obj): + return hasattr(obj, "_Value_value") + +Value = struct( + TYPEDEF = _Value_typedef, + new = _Value_new, + kwargs = _Value_kwargs, + set = _Value_set, +) + +def _UniqueList_typedef(): + """A mutable list of unique values. + + Value are kept in insertion order. + + :::{function} update(*others) -> None + ::: + + :::{function} build() -> list + """ + +def _UniqueList_new(kwargs, name): + """Builder for list of unique values. + + Args: + kwargs: {type}`dict[str, Any]` kwargs to search for `name` + name: {type}`str` A key in `kwargs` to initialize the value + to. If present, kwargs will be modified in place. + initial: {type}`list | None` The initial values. + + Returns: + {type}`UniqueList` + """ + + # TODO - Use set builtin instead of dict, when available. + # https://bazel.build/rules/lib/core/set + initial = {v: None for v in kwargs_pop_list(kwargs, name)} + + # buildifier: disable=uninitialized + self = struct( + _values = initial, + update = lambda *a, **k: _UniqueList_update(self, *a, **k), + build = lambda *a, **k: _UniqueList_build(self, *a, **k), + ) + return self + +def _UniqueList_build(self): + """Builds the values into a list + + Returns: + {type}`list` + """ + return self._values.keys() + +def _UniqueList_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 + +UniqueList = struct( + TYPEDEF = _UniqueList_typedef, + new = _UniqueList_new, +) diff --git a/python/private/py_binary_rule.bzl b/python/private/py_binary_rule.bzl index 9fa9061111..cdafced216 100644 --- a/python/private/py_binary_rule.bzl +++ b/python/private/py_binary_rule.bzl @@ -14,7 +14,6 @@ """Rule implementation of py_binary for Bazel.""" load(":attributes.bzl", "AGNOSTIC_BINARY_ATTRS") -load(":builders.bzl", "builders") load( ":py_executable.bzl", "create_executable_rule_builder", diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index b57ae47930..acd57b1b13 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -18,6 +18,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") 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", @@ -27,10 +28,7 @@ load( "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") @@ -51,7 +49,6 @@ load( "is_bool", "runfiles_root_path", "target_platform_has_any_constraint", - "union_attrs", ) load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag") load(":precompile.bzl", "maybe_precompile") @@ -61,7 +58,7 @@ load(":py_info.bzl", "PyInfo") load(":py_internal.bzl", "py_internal") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") -load(":rule_builders.bzl", "rule_builders") +load(":rule_builders.bzl", "ruleb") load( ":semantics.bzl", "ALLOWED_MAIN_EXTENSIONS", @@ -72,7 +69,6 @@ load( load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", - "TARGET_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE", ) @@ -81,12 +77,6 @@ _EXTERNAL_PATH_PREFIX = "external" _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 = dicts.add( @@ -96,7 +86,7 @@ EXECUTABLE_ATTRS = dicts.add( IMPORTS_ATTRS, COVERAGE_ATTRS, { - "legacy_create_init": lambda: rule_builders.IntAttrBuilder( + "legacy_create_init": lambda: attrb.Int( default = -1, values = [-1, 0, 1], doc = """\ @@ -113,7 +103,7 @@ the `srcs` of Python targets as required. # 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": lambda: rule_builders.LabelAttrBuilder( + "main": lambda: attrb.Label( allow_single_file = True, doc = """\ Optional; the name of the source file that is the main entry point of the @@ -122,7 +112,7 @@ application. This file must also be listed in `srcs`. If left unspecified, filename in `srcs`, `main` must be specified. """, ), - "pyc_collection": lambda: rule_builders.StringAttrBuilder( + "pyc_collection": lambda: attrb.String( default = PycCollectionAttr.INHERIT, values = sorted(PycCollectionAttr.__members__.values()), doc = """ @@ -137,7 +127,7 @@ Valid values are: target level. """, ), - "python_version": lambda: rule_builders.StringAttrBuilder( + "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 = """ @@ -163,25 +153,25 @@ accepting arbitrary Python versions. """, ), # Required to opt-in to the transition feature. - "_allowlist_function_transition": lambda: rule_builders.LabelAttrBuilder( + "_allowlist_function_transition": lambda: attrb.Label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), - "_bootstrap_impl_flag": lambda: rule_builders.LabelAttrBuilder( + "_bootstrap_impl_flag": lambda: attrb.Label( default = "//python/config_settings:bootstrap_impl", providers = [BuildSettingInfo], ), - "_bootstrap_template": lambda: rule_builders.LabelAttrBuilder( + "_bootstrap_template": lambda: attrb.Label( allow_single_file = True, default = "@bazel_tools//tools/python:python_bootstrap_template.txt", ), - "_launcher": lambda: rule_builders.LabelAttrBuilder( + "_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": lambda: rule_builders.LabelAttrBuilder( + "_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. @@ -196,24 +186,24 @@ accepting arbitrary Python versions. ##"_py_toolchain_type": attr.label( ## default = TARGET_TOOLCHAIN_TYPE, ##), - "_python_version_flag": lambda: rule_builders.LabelAttrBuilder( + "_python_version_flag": lambda: attrb.Label( default = "//python/config_settings:python_version", ), - "_venvs_use_declare_symlink_flag": lambda: rule_builders.LabelAttrBuilder( + "_venvs_use_declare_symlink_flag": lambda: attrb.Label( default = "//python/config_settings:venvs_use_declare_symlink", providers = [BuildSettingInfo], ), - "_windows_constraints": lambda: rule_builders.LabelListAttrBuilder( + "_windows_constraints": lambda: attrb.LabelList( default = [ "@platforms//os:windows", ], ), - "_windows_launcher_maker": lambda: rule_builders.LabelAttrBuilder( + "_windows_launcher_maker": lambda: attrb.Label( default = "@bazel_tools//tools/launcher:launcher_maker", cfg = "exec", executable = True, ), - "_zipper": lambda: rule_builders.LabelAttrBuilder( + "_zipper": lambda: attrb.Label( cfg = "exec", executable = True, default = "@bazel_tools//tools/zip:zipper", @@ -1750,16 +1740,19 @@ def create_base_executable_rule(): return create_executable_rule_builder().build() def create_executable_rule_builder(implementation, **kwargs): - builder = rule_builders.RuleBuilder( + builder = ruleb.Rule( implementation = implementation, attrs = EXECUTABLE_ATTRS, - exec_groups = REQUIRED_EXEC_GROUPS, + # todo: create builder for REQUIRED_EXEC_GROUPS, but keep the + # existing plain dict for now (Google uses it) + exec_groups = REQUIRED_EXEC_GROUP_BUILDERS, fragments = ["py", "bazel_py"], provides = [PyExecutableInfo], toolchains = [ - TOOLCHAIN_TYPE, - config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), - ] + _CC_TOOLCHAINS, + 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], diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index a837620304..a774104dd2 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,12 +33,11 @@ load( "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", "rule_builders") +load(":rule_builders.bzl", "ruleb") load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", @@ -53,10 +50,8 @@ 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", ), }, @@ -151,9 +146,10 @@ def create_py_library_rule_builder(*, attrs = {}, **kwargs): 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 @@ -162,14 +158,14 @@ def create_py_library_rule_builder(*, attrs = {}, **kwargs): # 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 {}) + kwargs["exec_groups"] = REQUIRED_EXEC_GROUP_BUILDERS | (kwargs.get("exec_groups") or {}) - builder = rule_builders.RuleBuilder( + builder = ruleb.Rule( attrs = dicts.add(LIBRARY_ATTRS, attrs), fragments = fragments + ["py"], toolchains = [ - config_common.toolchain_type(TOOLCHAIN_TYPE, mandatory = False), - config_common.toolchain_type(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), + ruleb.ToolchainType(TOOLCHAIN_TYPE, mandatory = False), + ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ], **kwargs ) diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index c1abd7822b..877cacde2d 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -1,4 +1,18 @@ -"""Builders for creating rules, aspects, attributes et al. +# 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 @@ -31,34 +45,38 @@ 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: attr_builders.LabelList(...), + "srcs": lambda: attrb.LabelList(...), } def create_foo_binary_builder(): - r = rule_builders.Rule( + foo = ruleb.Rule( executable = True, ) - r.implementation.set(_foo_binary_impl) - r.attrs.update(COMMON_ATTRS) - return r + foo.implementation.set(_foo_binary_impl) + foo.attrs.update(COMMON_ATTRS) + return foo def create_foo_test_builder(): - r = create_foo_binary_build() + foo = create_foo_binary_build() - binary_impl = r.implementation.get() + binary_impl = foo.implementation.get() def foo_test_impl(ctx): binary_impl(ctx) ... - r.implementation.set(foo_test_impl) - r.executable.set(False) - r.test.test(True) - r.attrs.update( - _coverage = attr_builders.Label(default="//:coverage") + foo.implementation.set(foo_test_impl) + foo.executable.set(False) + foo.test.test(True) + foo.attrs.update( + _coverage = attrb.Label(default="//:coverage") ) - return r + return foo foo_binary = create_foo_binary_builder().build() foo_test = create_foo_test_builder().build() @@ -76,156 +94,50 @@ custom_foo_binary = create_custom_foo_binary() """ load("@bazel_skylib//lib:types.bzl", "types") -load(":builders.bzl", "builders") - -def _to_kwargs_get_pairs(kwargs, obj): - ignore_names = {"extra_kwargs": None} - pairs = [] - for name in dir(obj): - if name in ignore_names or name in kwargs: - continue - value = getattr(obj, name) - if types.is_function(value): - continue # Assume it's a method - if _is_optional(value): - if not value.present(): - continue - else: - value = value.get() - - # NOTE: We can't call value.build() here: it would likely lead to - # recursion. - pairs.append((name, value)) - return pairs - -# To avoid recursion, this function shouldn't call `value.build()`. -# Recall that Bazel identifies recursion based on the (line, column) that -# a function (or lambda) is **defined** at -- the closure of variables -# is ignored. Thus, Bazel's recursion detection can be incidentally -# triggered if X.build() calls helper(), which calls Y.build(), which -# then calls helper() again -- helper() is indirectly recursive. -def _common_to_kwargs_nobuilders(self, kwargs = None): - if kwargs == None: - kwargs = {} - kwargs.update(self.extra_kwargs) - for name, value in _to_kwargs_get_pairs(kwargs, self): - kwargs[name] = value - - return kwargs - -def _Optional_typedef(): - """A wrapper for a re-assignable value that may or may not be set. - - This allows structs to have attributes whose values can be re-assigned, - e.g. ints, strings, bools, or values where the presence matteres. - """ - -def _Optional_new(*initial): - """Creates an instance. - - Args: - *initial: Either zero, one, or two positional args to set the - initial value stored for the optional. - - If zero args, then no value is stored. - - If one arg, then the arg is the value stored. - - If two args, then the first arg is a kwargs dict, and the - second arg is a name in kwargs to look for. If the name is - present in kwargs, it is removed from kwargs and its value - stored, otherwise kwargs is unmodified and no value is stored. - - Returns: - {type}`Optional` - """ - if len(initial) > 2: - fail("Only zero, one, or two positional args allowed, but got: {}".format(initial)) - - if len(initial) == 2: - kwargs, name = initial - if name in kwargs: - initial = [kwargs.pop(name)] - else: - initial = [] - else: - initial = list(initial) - - # buildifier: disable=uninitialized - self = struct( - # Length zero when no value; length one when has value. - _value = 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 _is_optional(obj): - return hasattr(obj, "present") - -Optional = struct( - TYPEDEF = _Optional_typedef, - new = _Optional_new, - get = _Optional_get, - set = _Optional_set, - present = _Optional_present, +load( + ":builders_util.bzl", + "UniqueList", + "Value", + "common_to_kwargs_nobuilders", + "kwargs_pop_dict", + "kwargs_pop_doc", + "kwargs_pop_list", + "to_kwargs_get_pairs", ) def _ExecGroup_typedef(): - """Builder for {obj}`exec_group()` + """Builder for {external:bzl:obj}`exec_group` :::{field} toolchains - :type: list[rule_builders.ToolchainType] + :type: list[ToolchainType] ::: :::{field} exec_compatible_with :type: list[str] ::: + :::{field} extra_kwargs + :type: dict[str, object] + ::: + :::{function} build() -> exec_group ::: """ 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` + """ self = struct( - toolchains = _kwargs_pop_list(kwargs, "toolchains"), - # List of strings - exec_compatible_with = _kwargs_pop_list(kwargs, "exec_compatible_with"), - build = lambda: exec_group(**_common_to_kwargs_nobuilders(self)), + toolchains = kwargs_pop_list(kwargs, "toolchains"), + exec_compatible_with = kwargs_pop_list(kwargs, "exec_compatible_with"), + extra_kwargs = kwargs, + build = lambda: exec_group(**common_to_kwargs_nobuilders(self)), ) return self @@ -242,29 +154,50 @@ def _ToolchainType_typedef(): ::: :::{field} mandatory - :type: Optional[bool] + :type: Value[bool] ::: :::{field} name - :type: Optional[str | Label] - ::: - - :::{function} build() -> config_common.toolchain_type + :type: Value[str | Label | None] ::: """ -def _ToolchainType_new(**kwargs): +def _ToolchainType_new(name = None, **kwargs): + """Creates a builder for `config_common.toolchain_type`. + + Args: + name: {type}`str | Label` the `toolchain_type` target this creates + a dependency to. + **kwargs: Same as {obj}`config_common.toolchain_type` + + Returns: + {type}`ToolchainType` + """ self = struct( - build = lambda: config_common.toolchain_type(**_common_to_kwargs_nobuilders(self)), + build = lambda: _ToolchainType_build(self), extra_kwargs = kwargs, - mandatory = _Optional_new(kwargs, "mandatory"), - name = _Optional_new(kwargs, "name"), + mandatory = Value.kwargs(kwargs, "mandatory", True), + name = Value.new(name), ) return self +def _ToolchainType_build(self): + """Builds a `config_common.toolchain_type` + + Args: + self: implicitly added + + Returns: + {type}`config_common.toolchain_type` + """ + kwargs = common_to_kwargs_nobuilders(self) + name = kwargs.pop("name") # Name must be positional + return config_common.toolchain_type(name, **kwargs) + ToolchainType = struct( TYPEDEF = _ToolchainType_typedef, new = _ToolchainType_new, + build = _ToolchainType_build, ) def _RuleCfg_typedef(): @@ -275,15 +208,23 @@ def _RuleCfg_typedef(): ::: :::{field} inputs - :type: SetBuilder + :type: UniqueList[Label] ::: :::{field} outputs - :type: SetBuilder + :type: UniqueList[Label] ::: """ def _RuleCfg_new(kwargs): + """Creates a builder for the `rule.cfg` arg. + + Args: + kwargs: Same args as `rule.cfg` + + Returns: + {type}`RuleCfg` + """ if kwargs == None: kwargs = {} @@ -292,16 +233,8 @@ def _RuleCfg_new(kwargs): build = lambda: _RuleCfg_build(self), extra_kwargs = kwargs, implementation = lambda: _RuleCfg_implementation(self), - # 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(kwargs, "inputs"), - # Bazel requires transition.outputs 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(kwargs, "outputs"), + inputs = UniqueList.new(kwargs, "inputs"), + outputs = UniqueList.new(kwargs, "outputs"), set_implementation = lambda *a, **k: _RuleCfg_set_implementation(self, *a, **k), ) return self @@ -362,19 +295,25 @@ def _Rule_typedef(): :type: RuleCfg ::: + :::{field} doc + :type: Value[str] + ::: + :::{field} exec_groups :type: dict[str, ExecGroup] ::: :::{field} executable - :type: Optional[bool] + :type: Value[bool] ::: :::{field} extra_kwargs :type: dict[str, Any] Additional keyword arguments to use when constructing the rule. Their - values have precedence when creating the rule kwargs. + values have precedence when creating the rule kwargs. This is, essentially, + an escape hatch for manually overriding or inserting values into + the args passed to `rule()`. ::: :::{field} fragments @@ -382,7 +321,7 @@ def _Rule_typedef(): ::: :::{field} implementation - :type: Optional[callable] + :type: Value[callable | None] ::: :::{field} provides @@ -390,7 +329,7 @@ def _Rule_typedef(): ::: :::{field} test - :type: Optional[bool] + :type: Value[bool] ::: :::{field} toolchains @@ -412,14 +351,15 @@ def _Rule_new(implementation = None, **kwargs): self = struct( attrs = _AttrsDict_new(kwargs.pop("attrs", None)), cfg = _RuleCfg_new(kwargs.pop("cfg", None)), - exec_groups = _kwargs_pop_dict(kwargs, "exec_groups"), - executable = _Optional_new(kwargs, "executable"), - fragments = _kwargs_pop_list(kwargs, "fragments"), - implementation = _Optional_new(implementation), + doc = kwargs_pop_doc(kwargs), + exec_groups = kwargs_pop_dict(kwargs, "exec_groups"), + executable = Value.kwargs(kwargs, "executable", False), + fragments = kwargs_pop_list(kwargs, "fragments"), + implementation = Value.new(implementation), extra_kwargs = kwargs, - provides = _kwargs_pop_list(kwargs, "provides"), - test = _Optional_new(kwargs, "test"), - toolchains = _kwargs_pop_list(kwargs, "toolchains"), + provides = kwargs_pop_list(kwargs, "provides"), + test = Value.kwargs(kwargs, "test", False), + toolchains = kwargs_pop_list(kwargs, "toolchains"), build = lambda *a, **k: _Rule_build(self, *a, **k), to_kwargs = lambda *a, **k: _Rule_to_kwargs(self, *a, **k), ) @@ -455,8 +395,23 @@ def _Rule_to_kwargs(self): Returns: {type}`dict` """ + kwargs = dict(self.extra_kwargs) - for name, value in _to_kwargs_get_pairs(kwargs, self): + if "exec_groups" not in kwargs: + for k, v in self.exec_groups.items(): + if not hasattr(v, "build"): + fail("bad execgroup", k, v) + kwargs["exec_groups"] = { + k: v.build() + for k, v in self.exec_groups.items() + } + if "toolchains" not in kwargs: + kwargs["toolchains"] = [ + v.build() + for v in self.toolchains + ] + + for name, value in to_kwargs_get_pairs(self, kwargs): value = value.build() if hasattr(value, "build") else value kwargs[name] = value return kwargs @@ -488,6 +443,15 @@ def _AttrsDict_typedef(): """ def _AttrsDict_new(initial): + """Creates a builder for the `rule.attrs` dict. + + Args: + initial: {type}`dict[str, callable | AttributeBuilder]` dict of initial + values to populate the attributes dict with. + + Returns: + {type}`AttrsDict` + """ self = struct( values = {}, update = lambda *a, **k: _AttrsDict_update(self, *a, **k), @@ -524,11 +488,7 @@ def _AttrsDict_build(self): """ attrs = {} for k, v in self.values.items(): - if hasattr(v, "build"): - v = v.build() - if not type(v) == "Attribute": - fail("bad attr type:", k, type(v), v) - attrs[k] = v + attrs[k] = v.build() if hasattr(v, "build") else v return attrs AttrsDict = struct( @@ -538,532 +498,8 @@ AttrsDict = struct( build = _AttrsDict_build, ) -# todo: move to another file; rename Set -def _SetBuilder(kwargs, name): - """Builder for list of unique values. - - Args: - kwargs: {type}`dict[str, Any]` kwargs to search for `name` - name: {type}`str` A key in `kwargs` to initialize the value - to. If present, kwargs will be modified in place. - initial: {type}`list | None` The initial values. - - Returns: - {type}`SetBuilder` - """ - initial = {v: None for v in _kwargs_pop_list(kwargs, name)} - - # 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 _kwargs_pop_dict(kwargs, key): - return dict(kwargs.pop(key, None) or {}) - -def _kwargs_pop_list(kwargs, key): - return list(kwargs.pop(key, None) or []) - -def _BoolAttr(**kwargs): - """Create a builder for attributes. - - Returns: - {type}`BoolAttr` - """ - - # buildifier: disable=uninitialized - self = struct( - default = _Optional_new(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - extra_kwargs = kwargs, - build = lambda: attr.bool(**_common_to_kwargs_nobuilders(self)), - ) - return self - -def _IntAttr(**kwargs): - # buildifier: disable=uninitialized - self = struct( - default = _Optional_new(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - values = kwargs.get("values") or [], - build = lambda *a, **k: _IntAttr_build(self, *a, **k), - extra_kwargs = kwargs, - ) - return self - -def _IntAttr_build(self): - kwargs = _common_to_kwargs_nobuilders(self) - return attr.int(**kwargs) - -def _AttrCfg_typedef(): - """Builder for `cfg` arg of label attributes. - - :::{function} implementation() -> callable - - Returns the implementation function for using custom transition. - ::: - - :::{field} outputs - :type: SetBuilder[str | Label] - ::: - - :::{field} inputs - :type: SetBuilder[str | Label] - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - """ - -def _AttrCfg_new(outer_kwargs, name): - """Creates an instance. - - Args: - outer_kwargs: {type}`dict` the kwargs to look for `name` within. - name: {type}`str` a key to look for in `outer_kwargs` for the - values to initilize from. If present in `outer_kwargs`, it - will be removed and the value initializes the builder. The value - is allowed to be one of: - - The string `exec` or `target` - - A dict with key `implementation`, which is a transition - implementation function. - - A dict with key `exec_group`, which is a string name for an - exec group to use for an exec transition. - - Returns: - {type}`AttrCfg` - """ - cfg = outer_kwargs.pop(name, None) - if cfg == None: - kwargs = {} - elif types.is_string(cfg): - kwargs = {"cfg": cfg} - else: - # Assume its a dict - kwargs = cfg - - if "cfg" in kwargs: - initial = kwargs.pop("cfg") - is_exec = False - elif "exec_group" in kwargs: - initial = kwargs.pop("exec_group") - is_exec = True - else: - initial = None - is_exec = False - - self = struct( - # list of (value, bool is_exec) - _implementation = [initial, is_exec], - implementation = lambda: self._implementation[0], - set_implementation = lambda *a, **k: _AttrCfg_set_implementation(self, *a, **k), - set_exec = lambda *a, **k: _AttrCfg_set_exec(self, *a, **k), - set_target = lambda: _AttrCfg_set_implementation(self, "target"), - exec_group = lambda: _AttrCfg_exec_group(self), - outputs = _SetBuilder(kwargs, "outputs"), - inputs = _SetBuilder(kwargs, "inputs"), - build = lambda: _AttrCfg_build(self), - extra_kwargs = kwargs, - ) - return self - -def _AttrCfg_set_implementation(self, impl): - """Sets a custom transition function to use. - - Args: - impl: {type}`callable` a transition implementation function. - """ - self._implementation[0] = impl - self._implementation[1] = False - -def _AttrCfg_set_exec(self, exec_group = None): - """Sets to use an exec transition. - - Args: - exec_group: {type}`str | None` the exec group name to use, if any. - """ - self._implementation[0] = exec_group - self._implementation[1] = True - -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. - - """ - if self._implementation[1]: - return self._implementation[0] - else: - return None - -def _AttrCfg_build(self): - value, is_exec = self._implementation - if value == None: - return None - elif is_exec: - return config.exec(value) - elif value == "target": - return config.target() - elif types.is_function(value): - return transition( - implementation = value, - inputs = self.inputs.build(), - outputs = self.outputs.build(), - ) - else: - # Otherwise, just assume the value is valid and whoever set it - # knows what they're doing. - return value - -AttrCfg = struct( - TYPEDEF = _AttrCfg_typedef, - new = _AttrCfg_new, - set_implementation = _AttrCfg_set_implementation, - set_exec = _AttrCfg_set_exec, - exec_group = _AttrCfg_exec_group, -) - -def _LabelAttr_typedef(): - """Builder for `attr.label` objects. - - :::{field} default - :type: Optional[str | label | configuration_field | None] - ::: - - :::{field} doc - :type: str - ::: - - :::{field} mandatory - :type: Optional[bool] - ::: - - :::{field} executable - :type: Optional[bool] - ::: - - :::{field} allow_files - :type: Optional[bool | list[str]] - ::: - - :::{field} allow_single_file - :type: Optional[bool] - ::: - - :::{field} providers - :type: list[provider | list[provider]] - ::: - - :::{field} cfg - :type: AttrCfg - ::: - - :::{field} aspects - :type: list[aspect] - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - """ - -def _LabelAttr_new(**kwargs): - """Creates an instance. - - Args: - **kwargs: The same as `attr.label()`. - - Returns: - {type}`LabelAttr` - """ - - # buildifier: disable=uninitialized - self = struct( - default = _Optional_new(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - executable = _Optional_new(kwargs, "executable"), - allow_files = _Optional_new(kwargs, "allow_files"), - allow_single_file = _Optional_new(kwargs, "allow_single_file"), - providers = _kwargs_pop_list(kwargs, "providers"), - cfg = _AttrCfg_new(kwargs, "cfg"), - aspects = _kwargs_pop_list(kwargs, "aspects"), - build = lambda: _LabelAttr_build(self), - extra_kwargs = kwargs, - ) - return self - -def _LabelAttr_build(self): - kwargs = { - "aspects": [v.build() for v in self.aspects], - } - _common_to_kwargs_nobuilders(self, kwargs) - for name, value in kwargs.items(): - kwargs[name] = value.build() if hasattr(value, "build") else value - return attr.label(**kwargs) - -LabelAttr = struct( - TYPEDEF = _LabelAttr_typedef, - new = _LabelAttr_new, - build = _LabelAttr_build, -) - -def _LabelListAttr_typedef(): - """Builder for `attr.label_list` - - :::{field} default - :type: Optional[list[str|Label] | configuration_field] - ::: - - :::{field} doc - :type: Optional[str] - ::: - - :::{field} mandatory - :type: Optional[bool] - ::: - - :::{field} executable - :type: Optional[bool] - ::: - - :::{field} allow_files - :type: Optional[bool | list[str]] - ::: - - :::{field} allow_empty - :type: Optional[bool] - ::: - - :::{field} providers - :type: list[provider | list[provider]] - ::: - - :::{field} cfg - :type: AttrCfg - ::: - - :::{field} aspects - :type: list[aspect] - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - """ - -def _LabelListAttr_new(**kwargs): - self = struct( - default = _kwargs_pop_list(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - executable = _Optional_new(kwargs, "executable"), - allow_empty = _Optional_new(kwargs, "allow_empty"), - allow_files = _Optional_new(kwargs, "allow_files"), - providers = _kwargs_pop_list(kwargs, "providers"), - cfg = _AttrCfg_new(kwargs, "cfg"), - aspects = _kwargs_pop_list(kwargs, "aspects"), - build = lambda: _LabelListAttr_build(self), - extra_kwargs = kwargs, - ) - return self - -def _LabelListAttr_build(self): - kwargs = _common_to_kwargs_nobuilders(self) - for key, value in kwargs.items(): - kwargs[key] = value.build() if hasattr(value, "build") else value - return attr.label_list(**kwargs) - -LabelListAttr = struct( - TYPEDEF = _LabelListAttr_typedef, - new = _LabelListAttr_new, - build = _LabelListAttr_build, -) - -def _StringListAttr_typedef(): - """Builder for `attr.string_list` - - :::{field} default - :type: Optiona[list[str] | configuration_field] - ::: - - :::{field} doc - :type: Optional[str] - ::: - - :::{field} mandatory - :type: Optional[bool] - ::: - - :::{field} allow_empty - :type: Optional[bool] - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - - :::{function} build() -> attr.string_list - ::: - """ - -def _StringListAttr_new(**kwargs): - self = struct( - default = _Optional_new(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - allow_empty = _Optional_new(kwargs, "allow_empty"), - build = lambda *a, **k: attr.string_list(**_common_to_kwargs_nobuilders(self, *a, **k)), - extra_kwargs = kwargs, - ) - return self - -StringList = struct( - TYPEDEF = _StringList_typedef, - new = _StringList_new, -) - -def _StringAttr_typedef(): - """Builder for `attr.string` - - :::{field} default - :type: Optiona[str] - ::: - - :::{field} doc - :type: Optiona[str] - ::: - :::{field} mandatory - :type: Optiona[bool] - ::: - - :::{field} allow_empty - :type: Optiona[bool] - ::: - - :::{function} build() -> attr.string - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - - :::{field} values - :type: list[str] - ::: - """ - -def _StringAttr_new(**kwargs): - self = struct( - default = _Optional_new(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - # True, False, or list - allow_empty = _Optional_new(kwargs, "allow_empty"), - build = lambda *a, **k: attr.string(**_common_to_kwargs_nobuilders(self, *a, **k)), - extra_kwargs = kwargs, - values = _kwargs_pop_list(kwargs, "values"), - ) - return self - -StringAttr = struct( - TYPEDEF = _StringAttr_typedef, - new = _StringAttr_new, -) - -def _StringDictAttr_typedef(): - """Builder for `attr.string_dict` - - :::{field} default - :type: dict[str, str], - ::: - - :::{field} doc - :type: Optional[str] - ::: - - :::{field} mandatory - :type: Optional[bool] - ::: - - :::{field} allow_empty - :type: Optional[bool] - ::: - - :::{function} build() -> attr.string_dict - ::: - - :::{field} extra_kwargs - :type: dict[str, Any] - ::: - """ - -def _StringDictAttr_new(**kwargs): - """Creates an instance. - - Args: - **kwargs: {type}`dict` The same args as for `attr.string_dict`. - - Returns: - {type}`StringDictAttr` - """ - self = struct( - default = _kwargs_pop_dict(kwargs, "default"), - doc = _Optional_new(kwargs, "doc"), - mandatory = _Optional_new(kwargs, "mandatory"), - allow_empty = _Optional_new(kwargs, "allow_empty"), - build = lambda: attr.string_dict(**_common_to_kwargs_nobuilders(self)), - extra_kwargs = kwargs, - ) - return self - -StringDictAttr = struct( - TYPEDEF = _StringDictAttr_typedef, - new = _StringDictAttr_new, -) - -# todo: remove Builder suffixes -# todo: move attr classes: attr_builders.LabelList -rule_builders = struct( +ruleb = struct( Rule = _Rule_new, - LabelAttr = _LabelAttr_new, - LabelListAttr = _LabelListAttr, - IntAttr = _IntAttr, - StringListAttr = _StringListAttr, - StringAttr = _StringAttr, - StringDictAttr = _StringDictAttr, - BoolAttr = _BoolAttr, - AttrCfg = _AttrCfg, + ToolchainType = _ToolchainType_new, + ExecGroup = _ExecGroup_new, ) diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt index 969c772386..05ea8934c5 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/inventories/bazel_inventory.txt @@ -60,6 +60,7 @@ ctx.workspace_name bzl:obj 1 rules/lib/builtins/ctx#workspace_name - 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/scratch/defs1.bzl b/tests/scratch/defs1.bzl index ab17190262..863eafdef9 100644 --- a/tests/scratch/defs1.bzl +++ b/tests/scratch/defs1.bzl @@ -1,5 +1,3 @@ -load(":defs2.bzl", "D", "THING") - def recursive_build(top): top_res = {} diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index d215ba7347..df6cbc990d 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -18,6 +18,7 @@ without the overhead of a bazel-in-bazel integration test. """ 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_OUTPUTS = _RECONFIG_INPUTS + [ _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,8 +66,8 @@ to make the RBE presubmits happy, which disable auto-detection of a CC 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, is_bin = False): From bcd44359e539014a477462a03b0ae371cb5d6274 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 4 Mar 2025 19:56:01 -0800 Subject: [PATCH 07/13] add bazel sphinx inventory --- sphinxdocs/inventories/bazel_inventory.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/inventories/bazel_inventory.txt index 05ea8934c5..dc11f02b5b 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/inventories/bazel_inventory.txt @@ -15,10 +15,17 @@ Target bzl:type 1 rules/lib/builtins/Target - 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 - From f5af97352d316966fe3048e1a189f9ac139248c6 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 4 Mar 2025 21:23:56 -0800 Subject: [PATCH 08/13] basic tests --- tests/builders/BUILD.bazel | 6 ++ tests/builders/attr_builders_tests.bzl | 84 ++++++++++++++++++++++ tests/builders/rule_builders_tests.bzl | 97 ++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 tests/builders/attr_builders_tests.bzl create mode 100644 tests/builders/rule_builders_tests.bzl diff --git a/tests/builders/BUILD.bazel b/tests/builders/BUILD.bazel index 3ad0c3e80c..c79381ff28 100644 --- a/tests/builders/BUILD.bazel +++ b/tests/builders/BUILD.bazel @@ -12,6 +12,12 @@ # 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") diff --git a/tests/builders/attr_builders_tests.bzl b/tests/builders/attr_builders_tests.bzl new file mode 100644 index 0000000000..8024ebd59f --- /dev/null +++ b/tests/builders/attr_builders_tests.bzl @@ -0,0 +1,84 @@ +# 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. + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects", "truth") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility + +_tests = [] + +objs = {} + +def _report_failures(name, env): + failures = env.failures + + def _report_failures_impl(env, target): + env._failures.extend(failures) + + analysis_test( + name = name, + target = "//python:none", + impl = _report_failures_impl, + ) + +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 _test_bool_defaults(name): + env, expect = _loading_phase_expect(name) + subject = attrb.Bool() + expect.that_str(subject.doc.get()).equals("") + expect.that_bool(subject.default.get()).equals(False) + expect.that_bool(subject.mandatory.get()).equals(False) + expect.that_dict(subject.extra_kwargs).contains_exactly({}) + + expect.that_str(str(subject.build())).contains("attr.bool") + _report_failures(name, env) + +_tests.append(_test_bool_defaults) + +def _test_bool_mutable(name): + subject = attrb.Bool() + subject.default.set(True) + subject.mandatory.set(True) + subject.doc.set("doc") + subject.extra_kwargs["extra"] = "value" + + env, expect = _loading_phase_expect(name) + expect.that_str(subject.doc.get()).equals("doc") + expect.that_bool(subject.default.get()).equals(True) + expect.that_bool(subject.mandatory.get()).equals(True) + expect.that_dict(subject.extra_kwargs).contains_exactly({"extra": "value"}) + + _report_failures(name, env) + +_tests.append(_test_bool_mutable) + +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 0000000000..3a72757573 --- /dev/null +++ b/tests/builders/rule_builders_tests.bzl @@ -0,0 +1,97 @@ +# 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. + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility +load("//python/private:rule_builders.bzl", "ruleb") # buildifier: disable=bzl-visibility + +BananaInfo = provider() + +def _banana_impl(ctx): + return [BananaInfo( + color = ctx.attr.color, + flavors = ctx.attr.flavors, + organic = ctx.attr.organic, + size = ctx.attr.size, + origin = ctx.attr.origin, + fertilizers = ctx.attr.fertilizers, + xx = mybool, + )] + +banana = ruleb.Rule( + implementation = _banana_impl, + attrs = { + "color": attrb.String(default = "yellow"), + "flavors": attrb.StringList(), + "organic": lambda: attrb.Bool(), + "size": lambda: attrb.Int(default = 10), + "origin": lambda: attrb.Label(), + "fertilizers": attrb.LabelList( + allow_files = True, + ), + }, +).build() + +_tests = [] + +mybool = attrb.Bool() + +def _test_basic_rule(name): + banana( + 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_basic_rule_impl, + ) + +def _test_basic_rule_impl(env, target): + info = target[BananaInfo] + env.expect.that_str(info.color).equals("yellow") + env.expect.that_collection(info.flavors).contains_exactly(["spicy", "sweet"]) + env.expect.that_bool(info.organic).equals(True) + env.expect.that_int(info.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(info.origin).label().equals(Label("//python/private:sentinel")) + + env.expect.that_collection(info.fertilizers).transform( + desc = "target.label", + map_each = lambda t: t.label, + ).contains_exactly([ + Label(":nitrogen.txt"), + Label(":phosphorus.txt"), + ]) + +_tests.append(_test_basic_rule) + +def rule_builders_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) From f12d146d3c6cd4bf51debe1302b25f7d2eca00f1 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 6 Mar 2025 14:44:22 -0800 Subject: [PATCH 09/13] add tests, convert to using getters/setters --- api_notes.md | 90 -- docs/_includes/field_kwargs_doc.md | 11 + notes.md | 321 ------- python/private/BUILD.bazel | 20 +- python/private/attr_builders.bzl | 1028 ++++++++++++++------- python/private/attributes.bzl | 102 +- python/private/builders_util.bzl | 352 ++----- python/private/common.bzl | 51 - python/private/py_binary_rule.bzl | 1 - python/private/py_executable.bzl | 16 +- python/private/rule_builders.bzl | 420 ++++++--- tests/builders/BUILD.bazel | 30 + tests/builders/attr_builders_tests.bzl | 428 ++++++++- tests/builders/rule_builders_tests.bzl | 233 ++++- tests/scratch/BUILD.bazel | 3 - tests/scratch/defs1.bzl | 63 -- tests/scratch/defs2.bzl | 15 - tests/support/empty_toolchain/BUILD.bazel | 3 + tests/support/empty_toolchain/empty.bzl | 23 + tests/support/sh_py_run_test.bzl | 8 +- 20 files changed, 1779 insertions(+), 1439 deletions(-) delete mode 100644 api_notes.md create mode 100644 docs/_includes/field_kwargs_doc.md delete mode 100644 notes.md delete mode 100644 tests/scratch/BUILD.bazel delete mode 100644 tests/scratch/defs1.bzl delete mode 100644 tests/scratch/defs2.bzl create mode 100644 tests/support/empty_toolchain/BUILD.bazel create mode 100644 tests/support/empty_toolchain/empty.bzl diff --git a/api_notes.md b/api_notes.md deleted file mode 100644 index 25148ab321..0000000000 --- a/api_notes.md +++ /dev/null @@ -1,90 +0,0 @@ - - -## For rule.cfg - -optional vs rule-union vs cfg-union? - -* Optional: feels verbose. Requires extra get() calls. -* Optional: seems harder to detect value -* Rule-union: which API feels verbose. -* Cfg-Union: seems nicest? More underlying impl work though. - -``` -# optional -# Rule.cfg is type Optional[TransitionBuilder | ConfigNone | ConfigTarget] - -r = RuleBuilder() -cfg = r.cfg.get() -if : - cfg.inputs.append(...) -elif : - ... -elif : - ... -else: error() - -# rule union -# Rule has {get,set}{cfg,cfg_none,cfg_target} functions -# which() tells which is set. -# Setting one clears the others - -r = RuleBuilder() -which = r.cfg_which() -if which == "cfg": - r.cfg().inputs.append(...) -elif which == "cfg_none": - ... -elif which == "cfg_target": - ... -else: error - -# cfg union (1) -# Rule.cfg is type RuleCfgBuilder -# RuleConfigBuilder has {get,set}{implementation,none,target} -# Setting one clears the others - -r = RuleBuilder() - -if r.cfg.implementation(): - r.cfg.inputs.append(...) -elif r.cfg.none(): - ... -elif r.cfg.target(): - ... -else: - error - -# cfg-union (2) -# Make implementation attribute polymorphic -impl = r.cfg.implementation() -if impl == "none": - ... -elif impl == "target": - ... -else: # function - r.cfg.inputs.append(...) - -# cfg-union (3) -# impl attr is an Optional -impl = r.cfg.implementation.get() -... r.cfg.implementation.set(...) ... -``` - -## Copies copies everywhere - -To have a nicer API, the builders should provide mutable lists/dicts/etc. - -But, when they accept a user input, they can't tell if the value is mutable or -not. So they have to make copies. Most of the time, the values probably _will_ -be mutable (why use a builder if its not mutable?). But its an easy mistake to -overlook that a list is referring to e.g. some global instead of a local var. - -So, we could defensively copy, or just document that a mutable input is -expected, and behavior is undefined otherwise. - -Alternatively, add a function to py_internal to detect immutability, and it'll -eventually be available in some bazel release. - -## Collections of of complex objects - -Should these be exposed as the raw collection, or a wrapper? e.g. diff --git a/docs/_includes/field_kwargs_doc.md b/docs/_includes/field_kwargs_doc.md new file mode 100644 index 0000000000..0241947b43 --- /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/notes.md b/notes.md deleted file mode 100644 index 59bf21f268..0000000000 --- a/notes.md +++ /dev/null @@ -1,321 +0,0 @@ -NOTES - -TLDR - -Two API choices to make: -1. (a) struct vs (b) dict -2. (a) less CPU/memory vs (b) nicer ergonomics - -Question: worth worrying about CPU/memory overhead? This is just loading -phase to construct the few dozen objects that are fed into rule creation - -EXAMPLE: CPU/memory vs ergonomics examples - -``` -# 1a + 2a: struct; uses less cpu/memory; worse ergonomics -def create_custom_rule() - r = py_binary_builder() - srcs = r.attrs["srcs"].to_mutable() - r.attrs["srcs"] = srcs - srcs.default.append("//bla") - cfg = srcs.cfg.get().to_mutable() - srcs.cfg.set(cfg) - cfg.inputs.append("whatever") - -# 1a+2b: struct; uses more cpu/memory; nicer ergonomics -def create_custom_rule() - r = py_binary_builder() - srcs = r.attrs["srcs"] - srcs.default.append("//bla") - srcs.cfg.inputs.append("whatever") - return r.build() - -# 1b+2a: dict; uses less cpu/memory; worse ergonomics -def create_custom_rule(): - r = py_binary_rule_kwargs() - srcs = dict(r["attrs"]["srcs"]) - r["attrs"]["srcs"] = srcs - srcs["default"] = list(srcs["default"]) - srcs["default"].append("//bla") - cfg = dict(srcs["cfg"]) - srcs["cfg"] = cfg - cfg["inputs"] = list(cfg["inputs"]) - cfg["inputs"].append("whatever") - - return rule(**r) - -# 1b+2b: dict; uses more cpu/memory; nicer ergonomics -def create_custom_rule(): - r = py_binary_rule_kwargs() - srcs = r["attrs"]["srcs"] - srcs["default"].append("//bla") - srcs["cfg"]["inputs"].append("whatever") - return rule(**r) - -``` - -Ergonomic highlights: -* Dicts don't need the `xx.{get,set}` stuff; you can just directly - assign without a wrapper. -* Structs don't need `x[key] = list(x[key])` stuff. They can ensure their - lists/dicts are mutable themselves. - * Can somewhat absolve this: just assume things are mutable -* Structs _feel_ more "solid". Like a real API. -* Structs give us API control; dicts don't. - ---------- - -Our goals are to allow users to derive new rules based upon ours. This translates to -two things: -* Specifying an implementation function. This allows them to introduce their own - logic. (Hooking into our impl function is for another time) -* Customizing the rule() kwargs. This is necessary because a different - implementation function almost certainly requires _something_ different in the - rule() kwargs. It may be new attributes or modifications to existing - attributes; we can't know and don't care. Most other rule() kwargs they want - to automatically inherit; they are either internal details or upstream - functionality they want to automatically benefit from. - -So, we need some way to for users to intercept the rule kwargs before they're -turned into immutable objects (attr.xxx, transition(), exec_group(), etc etc) -that they can't introspect or modify. - -Were we using a more traditional language, we'd probably have classes: -``` -class PyBinaryRule(Rule): ... -class UserPyBinaryRule(PyBinary): ... -``` - -And have various methods for how different pieces are created. - -We don't have classes or inheritence, though, so we have to find other avenues. - -==================== - -A key constraint are Bazel's immutability rules. - -* Objects are mutable within the thread that creates them. -* Each bzl file evaluation is a separate thread. - -Translated: -* Assigning a list/dict (_directly or indirectly_) to a global var makes it - immutable once the bzl file is finished being evaluated. -* A work around to this limitation is to use a function/lambda. When `foo.bzl` calls - `bar.bzl%create_builder()`, it is foo.bzl's thread creating _new_ objects, so - they are returned as mutable objects. - -Relatedly, this means mutability has to be "top down". e.g. given -`x: dict[str, dict[str, int]] = ...`, in order to modify -the inner dict, the outer dict must also be mutable. It's not possible -for an immutable object to reference a mutable object because Bazel -makes things recursively immutable when the thread ends. - -What this means for us: - -1. In order for us to expose objects users can modify, we _must_ provide them - with a function to create the objects they will modify. How they call that - function is up to us and defines our public API for this. -2. Whatever we expose, we cannot return immutable objects, e.g. `attr.string()`, - `transition()`, `exec_group()`, et al, or direct references to e.g. global - vars. Such objects are immutable, many cannot be introspected, and - immutability can't be detected; this prevents a user from customizing. - -==================== - -Unfortunately, everything we're dealing with is some sort of container whose -contents users may want to arbitrarily modify. A type-wise description -looks something like: - -``` -class Rule: - implementation: function - test: bool | unset - attrs: dict[str name, Attribute] - cfg: string | ExecGroup | Transition - -class LabelListAttribute(Attribute): - default: list[string | Label] - cfg: string | ExecGroup | Transition - -class Transition: - implementation: function - inputs: list[string] - outputs: list[string] - -``` - -Where calling e.g `Rule()` can be translated to using `struct(...)` or -`dict(...)` in Starlark. - -All these containers of values mean the top-down immutability rules are -prominent and affect the API. Lets discuss that next. - -==================== - -Recall: - -* Deep immutable: after calling `x = py_executable_builder()` - the result is a "mutable rule" object. Every part (i.e. dict/lists) of `x` - is mutable, recursively. e.g. `x.foo["y"].z.append(1)` works. -* Shallow immutable: after calling `x = py_executable_builder()`, - the result is a "mutable rule" object, but only the attributes/objects that - directly belong to it are _guaranteed_ mutable. e.g. - * works: `x.foo["y"] = ...` - * may not work: `x.foo["y"].z.append(1)` - -If it's deep mutable, then the user API is easy, but it costs more CPU to -create and costs more memory (equivalent objects are created multiple times). - -If it's shallow mutable, then the user API is harder. To allow mutability, -objects must provide a lambda to re-create themselves. Being a "builder" isn't -sufficient; it must have been created in the current thread context. - -Let's explore the implications of shallow vs deep immutability. - -1. Everything always deep immutable - -Each rule calls `create_xxx_rule_builder()`, the result is deep mutable. -* Pro: Easy -* Con: Wasteful. Most things aren't customized. Equivalent attributes, - transitions, etc objects get recreated instead of reused (Bazel doesn't do - anything smart with them when they're logically equivalent, and it can't - because each call carries various internal debug state info) - -2. Shallow immutability - -The benefit shallow immutability brings is objects (e.g. attr.xxx etc) are -only recreated when they're modified. - -Each rule calls `create_xxx_rule_builder`, the result is shallow mutable. e.g. -`x.attrs` is a mutable dict, but the values may not be mutable. If we want -to modify something deeper, the object has its `to_mutable()` method called. -This create a mutable version of the object (shallow or deep? read on). - -Under the hood, the way this works is objects have an immutable version of -themselves and function to create an equivalent mutable version of themselves. -e.g., creating an attribute looks like this: -``` -def Attr(builder_factory): - builder = builder_factory() - built = builder.build() - return struct(built=built, to_mutable = builder_factory) - -def LabelListBuilder(default): - self = struct( - default = default - to_mutable = lambda: self - build = lambda: attr.label(default=self.default) - ) - return self - -SRCS = Attr(lambda: LabelListBuilder(default=["a"])) -def base(): - builder.attrs["srcs"] = SRCS - -def custom(): - builder = base() - srcs = builder.attrs["srcs"].to_mutable() - srcs["default"].append("b") - builder.attrs["srcs"] = srcs -``` - -When the final `rule()` kwargs are created, the logic checks for obj.built and -uses it if present. Otherwise it calls e.g. `obj.build()` to create it. - -The disadvantage is the API is more complicated. You have to remember to call -`to_mutable()` and reassign the value. - -If the return value of `to_mutable()` is deep immutable, then this is as -complicated as the API gets. You just call it once, at the "top". - -If the return value of `to_mutable()` is _also_ shallow mutable, then this is -API complication is recursive in nature. e.g, lets say we want to modify the -inputs for an attributes's transition when things are shallow immutable: - -``` -def custom(): - builder = base() - srcs = builder.attrs["srcs"].to_mutable() # -> LabelListBuilder - cfg = srcs.cfg.to_mutable() # TransitionBuilder - cfg.inputs.append("bla") - srcs.cfg.set(cfg) # store our modified cfg back into the attribute - builder.attrs["srcs"] = srcs # store modified attr back into the rule attrs -``` - -Pretty tedious. - -Also, the nature of the top-down mutability constraint somewhat works against -the design goal here. We avoid having to recreate _all_ the objects for a rule, -but we still had to re-create the direct values that the srcs Attribute object -manages. So less work, but definitely not precise. - -3. Mix/Match of immutability - -A compromise between (1) and (2) is for `to_mutable()` to be shallow for some -things but deep for others. - -* Rule is shallow immutable. e.g. `Rule.attrs` is a mutable dict, but contains - immutable Attribute objects. -* Attribute.to_mutable returns a deep mutable object. This avoids having to - call to_mutable() many times and reassign up the object tree. - --------------------- - -Alternative: visitor pattern - -Instead of returning a mutable value to users to modify, users pass in a -Visitor object, which has methods to handle rule kwarg building. e.g. - -``` -SRCS = Attr(lambda: LabelListBuilder(...)) - -def create_executable_rule(visitor): - kwargs = visitor.init_kwargs(visitor) - kwargs["srcs"] = visitor.add_attr("srcs", SRCS.built, SRCS.to_mutable) - kwargs = visitor.finalize_kwargs(kwargs) - return rule(**kwargs) - -def visitor(): - return struct( - add_attr = visit_add_attr, - ... - ) - -def customize_add_attr(name, built, new_builder): - if name != "srcs": - return built - builder = new_builder() - builder.default.append("custom") - return builder.build() - -custom_rule = create_executable_rule(visitor()) -``` - -Unfortunately, this doesn't change things too much. The same issue of object -reuse vs immutability show up. This actually seems _worse_ because now -a user has to do pseudo-object-oriented Starlark for even small changes. - - --------------------- - -Alternative: overrride values - -The idea here is to return immutable values to benefit from better cpu/memory -usage. To modify something, a user calls a function to create a mutable version -of it, then overwrites the value entirely. - -``` -load(":base.bzl", "create_srcs_attr_builder", "create_cfg_builder") -def custom_rule(): - builder = base() - srcs = create_srcs_attr_builder() - srcs.providers.append("bla") - - cfg = create_cfg_builder() - cfg.inputs.append("bla") - builder.cfg.set(cfg) -``` - -This is similar to having a `to_mutable()` method. The difference is there are -no wrapper objects in between. e.g. the builder.attrs dict contains attr.xxx -objects, instead of `struct(built=, to_mutable=)` diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 0047dafad5..d61bb6a15e 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -110,15 +110,7 @@ bzl_library( ], ) -bzl_library( - name = "rule_builders_bzl", - srcs = ["rule_builders.bzl"], - deps = [ - ":builders_bzl", - ":builders_util_bzl", - "@bazel_skylib//lib:types", - ], -) + bzl_library( name = "bzlmod_enabled_bzl", @@ -595,6 +587,16 @@ bzl_library( srcs = ["repo_utils.bzl"], ) +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 index 93c3a2f5f9..aa68fc0f27 100644 --- a/python/private/attr_builders.bzl +++ b/python/private/attr_builders.bzl @@ -17,110 +17,156 @@ load("@bazel_skylib//lib:types.bzl", "types") load( ":builders_util.bzl", - "Optional", - "UniqueList", - "Value", - "common_to_kwargs_nobuilders", - "kwargs_pop_dict", - "kwargs_pop_doc", - "kwargs_pop_list", - "kwargs_pop_mandatory", + "kwargs_getter", + "kwargs_set_default_doc", + "kwargs_set_default_ignore_none", + "kwargs_set_default_list", + "kwargs_set_default_mandatory", + "kwargs_setter", + "to_label_maybe", ) -def _kwargs_pop_allow_empty(kwargs): - return Value.kwargs(kwargs, "allow_empty", True) +def _kwargs_set_default_allow_empty(kwargs): + existing = kwargs.get("allow_empty") + if existing == None: + kwargs["allow_empty"] = True + +def _kwargs_set_default_allow_files(kwargs): + existing = kwargs.get("allow_files") + if existing == None: + kwargs["allow_files"] = False + +def _common_label_build(self, attr_factory): + kwargs = dict(self.kwargs) + kwargs["cfg"] = self.cfg.build() + return attr_factory(**kwargs) def _AttrCfg_typedef(): """Builder for `cfg` arg of label attributes. - :::{function} implementation() -> callable | None - - Returns the implementation function when a custom transition is being used. + :::{function} inputs() -> list[Label] ::: - :::{field} outputs - :type: UniqueList[Label] + :::{function} outputs() -> list[Label] ::: - :::{field} inputs - :type: UniqueList[str | Label] - ::: + :::{function} which_cfg() -> str - :::{field} extra_kwargs - :type: dict[str, Any] + Tells which of the cfg modes is set. Will be one of: target, exec, none, + or implementation ::: """ -def _AttrCfg_new(outer_kwargs, name): +def _AttrCfg_new( + inputs = None, + outputs = None, + **kwargs): """Creates a builder for the `attr.cfg` attribute. Args: - outer_kwargs: {type}`dict` the kwargs to look for `name` within. - name: {type}`str` a key to look for in `outer_kwargs` for the - values to initilize from. If present in `outer_kwargs`, it - will be removed and the value initializes the builder. The value - is allowed to be one of: - - The string `exec` or `target` - - A dict with key `implementation`, which is a transition - implementation function. - - A dict with key `exec_group`, which is a string name for an - exec group to use for an exec transition. + 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` """ - cfg = outer_kwargs.pop(name, None) - if cfg == None: - kwargs = {} - elif types.is_string(cfg): - kwargs = {"cfg": cfg} - else: - # Assume its a dict - kwargs = cfg - - if "cfg" in kwargs: - initial = kwargs.pop("cfg") - is_exec = False - elif "exec_group" in kwargs: - initial = kwargs.pop("exec_group") - is_exec = True - else: - initial = None - is_exec = False + state = { + "inputs": inputs, + "outputs": outputs, + # Value depends on "which" key + # For which=impl, the value is a function or arbitrary object + "value": True, + # str: target, exec, none, or impl + "which": "target", + } + kwargs_set_default_list(state, "inputs") + kwargs_set_default_list(state, "outputs") # buildifier: disable=uninitialized self = struct( - # list of (value, bool is_exec) - _implementation = [initial, is_exec], - implementation = lambda: self._implementation[0], - set_implementation = lambda *a, **k: _AttrCfg_set_implementation(self, *a, **k), - set_exec = lambda *a, **k: _AttrCfg_set_exec(self, *a, **k), - set_target = lambda: _AttrCfg_set_implementation(self, "target"), - exec_group = lambda: _AttrCfg_exec_group(self), - outputs = UniqueList.new(kwargs, "outputs"), - inputs = UniqueList.new(kwargs, "inputs"), + # keep sorted + _state = state, build = lambda: _AttrCfg_build(self), - extra_kwargs = kwargs, + 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, "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_set_implementation(self, impl): - """Sets a custom transition function to use. +def _AttrCfg_from_attr_kwargs_pop(attr_kwargs): + """Creates a `AttrCfg` from the cfg arg passed to an attribute bulider. Args: - impl: {type}`callable` a transition implementation function. + attr_kwargs: dict of attr kwargs, it's "cfg" key will be removed. + + Returns: + {type}`AttrCfg` """ - self._implementation[0] = impl - self._implementation[1] = False + cfg = attr_kwargs.pop("cfg", None) + if not types.is_dict(cfg): + kwargs = {"cfg": cfg} + else: + kwargs = cfg + return _AttrCfg_new(**kwargs) -def _AttrCfg_set_exec(self, exec_group = None): - """Sets to use an exec transition. +def _AttrCfg_implementation(self): + """Tells the custom transition function, if any and applicable. - Args: - exec_group: {type}`str | None` the exec group name to use, if any. + 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["value"] if self._state["which"] == "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["value"] if self._state["which"] == "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. """ - self._implementation[0] = exec_group - self._implementation[1] = True + return self._state["value"] if self._state["which"] == "target" else False def _AttrCfg_exec_group(self): """Tells the exec group to use if an exec transition is being used. @@ -129,62 +175,110 @@ def _AttrCfg_exec_group(self): self: implicitly added. Returns: - {type}`str | None` the name of the exec group to use if any. + {type}`str | None` the name of the exec group to use if any, + or `None` if `which_cfg` isn't `exec` + """ + return self._state["value"] if self._state["which"] == "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. """ - if self._implementation[1]: - return self._implementation[0] - else: - return None + self._state["which"] = "impl" + self._state["value"] = impl + +def _AttrCfg_set_none(self): + """Sets to use the "none" transition.""" + self._state["which"] = "none" + self._state["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["which"] = "exec" + self._state["value"] = exec_group + +def _AttrCfg_set_target(self): + """Sets to use the target transition.""" + self._state["which"] = "target" + self._state["value"] = True def _AttrCfg_build(self): - value, is_exec = self._implementation - if value == None: + which = self._state["which"] + value = self._state["value"] + if which == None: return None - elif is_exec: + elif which == "target": + # config.target is Bazel 8+ + if hasattr(config, "target"): + return config.target() + else: + return "target" + elif which == "exec": return config.exec(value) - elif value == "target": - return config.target() + elif which == "none": + return config.none() elif types.is_function(value): return transition( implementation = value, - inputs = self.inputs.build(), - outputs = self.outputs.build(), + # 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. + # 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, - set_implementation = _AttrCfg_set_implementation, - set_exec = _AttrCfg_set_exec, + # 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 fo attr.bool. + """Builder for attr.bool. :::{function} build() -> attr.bool ::: - :::{field} default - :type: Value[bool] + :::{function} default() -> bool. + ::: + + :::{function} doc() -> str ::: - :::{field} doc - :type: Value[str] + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool ::: - :::{field} extra_kwargs - :type: dict[str, object] + :::{function} set_default(v: bool) ::: + + :::{function} set_doc(v: str) + ::: + + :::{function} set_mandatory(v: bool) + ::: + """ def _Bool_new(**kwargs): @@ -196,17 +290,25 @@ def _Bool_new(**kwargs): 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( - default = Value.kwargs(kwargs, "default", False), - doc = kwargs_pop_doc(kwargs), - mandatory = kwargs_pop_mandatory(kwargs), - extra_kwargs = kwargs, - build = lambda: attr.bool(**common_to_kwargs_nobuilders(self)), + # keep sorted + build = lambda: attr.bool(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions Bool = struct( TYPEDEF = _Bool_typedef, new = _Bool_new, @@ -218,24 +320,30 @@ def _Int_typedef(): :::{function} build() -> attr.int ::: - :::{field} default - :type: Value[int] + :::{function} default() -> int ::: - :::{field} doc - :type: Value[str] + :::{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. ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{function} set_default(v: int) ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_doc(v: str) ::: - :::{field} values - :type: list[int] + :::{function} set_mandatory(v: bool) ::: """ @@ -248,18 +356,26 @@ def _Int_new(**kwargs): 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(**common_to_kwargs_nobuilders(self)), - default = Value.kwargs(kwargs, "default", 0), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), - values = kwargs_pop_list(kwargs, "values"), + build = lambda: attr.int(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + values = kwargs_getter(kwargs, "values"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions Int = struct( TYPEDEF = _Int_typedef, new = _Int_new, @@ -268,27 +384,31 @@ Int = struct( def _IntList_typedef(): """Builder for attr.int_list. - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{function} build() -> attr.int_list ::: - :::{field} default - :type: list[int] + :::{function} default() -> list[int] + ::: + + :::{function} doc() -> str + ::: + + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} mandatory() -> bool ::: - :::{field} doc - :type: Value[str] + :::{function} set_allow_empty(v: bool) ::: - :::{field} extra_kwargs - :type: dict[str, object] + :::{function} set_doc(v: str) ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_mandatory(v: bool) ::: """ @@ -301,18 +421,27 @@ def _IntList_new(**kwargs): 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( - allow_empty = _kwargs_pop_allow_empty(kwargs), - build = lambda: attr.int_list(**common_to_kwargs_nobuilders(self)), - default = kwargs_pop_list(kwargs, "default"), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + # keep sorted + allow_empty = kwargs_getter(kwargs, "allow_empty"), + build = lambda: attr.int_list(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions IntList = struct( TYPEDEF = _IntList_typedef, new = _IntList_new, @@ -321,50 +450,59 @@ IntList = struct( def _Label_typedef(): """Builder for `attr.label` objects. - :::{field} default - :type: Value[str | label | configuration_field | None] + :::{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. ::: - :::{field} doc - :type: Value[str] + :::{function} aspects() -> list[aspect] + + The returned list is a mutable reference to the underlying list. ::: - :::{field} mandatory - :type: Value[bool] + :::{function} build() -> attr.label ::: - :::{field} executable - :type: Value[bool] + :::{field} cfg + :type: AttrCfg ::: - :::{field} allow_files - :type: Optional[bool | list[str] | None] + :::{function} default() -> str | label | configuration_field | None + ::: - Note that `allow_files` is mutually exclusive with `allow_single_file`. - Only one of the two can have a value set. + :::{function} doc() -> str ::: - :::{field} allow_single_file - :type: Optional[bool | None] + :::{function} executable() -> bool + ::: - Note that `allow_single_file` is mutually exclusive with `allow_files`. - Only one of the two can have a value set. + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} providers - :type: list[provider | list[provider]] + :::{function} mandatory() -> bool ::: - :::{field} cfg - :type: AttrCfg + + :::{function} providers() -> list[list[provider]] + The returned list is a mutable reference to the underlying list. + ::: + + :::{function} set_default(v: str | Label) ::: - :::{field} aspects - :type: list[aspect] + :::{function} set_doc(v: str) ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{function} set_executable(v: bool) + ::: + + :::{function} set_mandatory(v: bool) ::: """ @@ -377,76 +515,137 @@ def _Label_new(**kwargs): Returns: {type}`Label` """ + kwargs_set_default_ignore_none(kwargs, "executable", False) + kwargs_set_default_list(kwargs, "aspects") + kwargs_set_default_list(kwargs, "providers") + kwargs_set_default_doc(kwargs) + kwargs_set_default_mandatory(kwargs) + + kwargs["default"] = to_label_maybe(kwargs.get("default")) # buildifier: disable=uninitialized self = struct( - default = Value.kwargs(kwargs, "default", None), - doc = kwargs_pop_doc(kwargs), - mandatory = kwargs_pop_mandatory(kwargs), - executable = Value.kwargs(kwargs, "executable", False), - allow_files = Optional.new(kwargs, "allow_files"), - allow_single_file = Optional.new(kwargs, "allow_single_file"), - providers = kwargs_pop_list(kwargs, "providers"), - cfg = _AttrCfg_new(kwargs, "cfg"), - aspects = kwargs_pop_list(kwargs, "aspects"), - build = lambda: _Label_build(self), - extra_kwargs = kwargs, + # keep sorted + add_allow_files = lambda v: _Label_add_allow_files(self, v), + allow_files = kwargs_getter(kwargs, "allow_files"), + allow_single_file = kwargs_getter(kwargs, "allow_single_file"), + aspects = kwargs_getter(kwargs, "aspects"), + build = lambda: _common_label_build(self, attr.label), + cfg = _AttrCfg_from_attr_kwargs_pop(kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + executable = kwargs_getter(kwargs, "executable"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + providers = kwargs_getter(kwargs, "providers"), + 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(kwargs, "doc"), + set_executable = kwargs_setter(kwargs, "executable"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self -def _Label_build(self): - kwargs = dict(self.extra_kwargs) - if "aspects" not in kwargs: - kwargs["aspects"] = [v.build() for v in self.aspects] +def _Label_set_allow_files(self, v): + """Set the allow_files arg + + NOTE: Setting `allow_files` unsets `allow_single_file` - common_to_kwargs_nobuilders(self, kwargs) - for name, value in kwargs.items(): - kwargs[name] = value.build() if hasattr(value, "build") else value - return attr.label(**kwargs) + 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, - build = _Label_build, + 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. - :::{field} aspects - :type: list[aspect] + :::{function} aspects() -> list[aspect] + The returned list is a mutable reference to the underlying list. ::: - :::{field} allow_files - :type: Value[bool | list[str]] + :::{function} allow_files() -> bool | list[str] ::: - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{field} cfg :type: AttrCfg ::: - :::{field} default - :type: Value[dict[str|Label, str] | callable] + :::{function} default() -> dict[str | Label, str] | callable ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool ::: - :::{field} providers - :type: list[provider | list[provider]] + :::{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]) ::: """ @@ -459,64 +658,102 @@ def _LabelKeyedStringDict_new(**kwargs): Returns: {type}`LabelKeyedStringDict` """ + kwargs_set_default_ignore_none(kwargs, "default", {}) + kwargs_set_default_list(kwargs, "aspects") + kwargs_set_default_list(kwargs, "providers") + _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( - allow_empty = _kwargs_pop_allow_empty(kwargs), - allow_files = Value.kwargs(kwargs, "allow_files", False), - aspects = kwargs_pop_list(kwargs, "aspects"), - build = lambda: _LabelList_build(self), - cfg = _AttrCfg_new(kwargs, "cfg"), - default = Value.kwargs(kwargs, "default", {}), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), - providers = kwargs_pop_list(kwargs, "providers"), + # keep sorted + add_allow_files = lambda *v: _LabelKeyedStringDict_add_allow_files(self, *v), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + allow_files = kwargs_getter(kwargs, "allow_files"), + aspects = kwargs_getter(kwargs, "aspects"), + 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(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + providers = kwargs_getter(kwargs, "providers"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_allow_files = kwargs_setter(kwargs, "allow_files"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) 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` - :::{field} aspects - :type: list[aspect] + :::{function} aspects() -> list[aspect] ::: - :::{field} allow_files - :type: Value[bool | list[str]] + :::{function} allow_files() -> bool | list[str] ::: - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool + ::: + + :::{function} build() -> attr.label_list ::: :::{field} cfg :type: AttrCfg ::: - :::{field} default - :type: Value[list[str|Label] | configuration_field | callable] + :::{function} default() -> list[str|Label] | configuration_field | callable ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool ::: - :::{field} providers - :type: list[provider | list[provider]] + :::{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) ::: """ @@ -529,34 +766,40 @@ def _LabelList_new(**kwargs): 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_list(kwargs, "aspects") + kwargs_set_default_list(kwargs, "default") + kwargs_set_default_list(kwargs, "providers") # buildifier: disable=uninitialized self = struct( - allow_empty = _kwargs_pop_allow_empty(kwargs), - allow_files = Value.kwargs(kwargs, "allow_files", False), - aspects = kwargs_pop_list(kwargs, "aspects"), - build = lambda: _LabelList_build(self), - cfg = _AttrCfg_new(kwargs, "cfg"), - default = Value.kwargs(kwargs, "default", []), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), - providers = kwargs_pop_list(kwargs, "providers"), + # keep sorted + allow_empty = kwargs_getter(kwargs, "allow_empty"), + allow_files = kwargs_getter(kwargs, "allow_files"), + aspects = kwargs_getter(kwargs, "aspects"), + build = lambda: _common_label_build(self, attr.label_list), + cfg = _AttrCfg_from_attr_kwargs_pop(kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + providers = kwargs_getter(kwargs, "providers"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_allow_files = kwargs_setter(kwargs, "allow_files"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self -def _LabelList_build(self): - """Creates a {obj}`attr.label_list`.""" - - kwargs = common_to_kwargs_nobuilders(self) - for key, value in kwargs.items(): - kwargs[key] = value.build() if hasattr(value, "build") else value - return attr.label_list(**kwargs) - +# buildifier: disable=name-conventions LabelList = struct( TYPEDEF = _LabelList_typedef, new = _LabelList_new, - build = _LabelList_build, ) def _Output_typedef(): @@ -565,16 +808,19 @@ def _Output_typedef(): :::{function} build() -> attr.output ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str + ::: + + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} mandatory() -> bool ::: - :::{field} extra_kwargs - :type: dict[str, object] + :::{function} set_doc(v: str) ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_mandatory(v: bool) ::: """ @@ -587,16 +833,22 @@ def _Output_new(**kwargs): Returns: {type}`Output` """ + kwargs_set_default_doc(kwargs) + kwargs_set_default_mandatory(kwargs) # buildifier: disable=uninitialized self = struct( - build = lambda: attr.output(**common_to_kwargs_nobuilders(self)), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + # keep sorted + build = lambda: attr.output(**self.kwargs), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions Output = struct( TYPEDEF = _Output_typedef, new = _Output_new, @@ -605,23 +857,26 @@ Output = struct( def _OutputList_typedef(): """Builder for attr.output_list - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{function} build() -> attr.output ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} extra_kwargs - :type: dict[str, object] + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool + ::: + + :::{function} set_allow_empty(v: bool) + ::: + :::{function} set_doc(v: str) + ::: + :::{function} set_mandatory(v: bool) ::: """ @@ -634,17 +889,24 @@ def _OutputList_new(**kwargs): 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_pop_allow_empty(kwargs), - build = lambda: attr.output_list(**common_to_kwargs_nobuilders(self)), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + build = lambda: attr.output_list(**self.kwargs), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions OutputList = struct( TYPEDEF = _OutputList_typedef, new = _OutputList_new, @@ -656,24 +918,28 @@ def _String_typedef(): :::{function} build() -> attr.string ::: - :::{field} default - :type: Value[str | configuration_field] + :::{function} default() -> str | configuration_field + ::: + + :::{function} doc() -> str + ::: + + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} mandatory() -> bool ::: - :::{field} doc - :type: Value[str] + :::{function} values() -> list[str] ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{function} set_default(v: str | configuration_field) ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_doc(v: str) ::: - :::{field} values - :type: list[str] + :::{function} set_mandatory(v: bool) ::: """ @@ -686,18 +952,26 @@ def _String_new(**kwargs): 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 = Value.kwargs(kwargs, "default", ""), - doc = kwargs_pop_doc(kwargs), - mandatory = kwargs_pop_mandatory(kwargs), - build = lambda *a, **k: attr.string(**common_to_kwargs_nobuilders(self, *a, **k)), - extra_kwargs = kwargs, - values = kwargs_pop_list(kwargs, "values"), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + mandatory = kwargs_getter(kwargs, "mandatory"), + build = lambda: attr.string(**self.kwargs), + kwargs = kwargs, + values = kwargs_getter(kwargs, "values"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions String = struct( TYPEDEF = _String_typedef, new = _String_new, @@ -706,27 +980,29 @@ String = struct( def _StringDict_typedef(): """Builder for `attr.string_dict` - :::{field} default - :type: dict[str, str] + :::{function} default() -> dict[str, str] ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool ::: - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{function} build() -> attr.string_dict ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} set_doc(v: str) + ::: + :::{function} set_mandatory(v: bool) + ::: + :::{function} set_allow_empty(v: bool) ::: """ @@ -739,18 +1015,26 @@ def _StringDict_new(**kwargs): 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_pop_allow_empty(kwargs), - build = lambda: attr.string_dict(**common_to_kwargs_nobuilders(self)), - default = kwargs_pop_dict(kwargs, "default"), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + build = lambda: attr.string_dict(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions StringDict = struct( TYPEDEF = _StringDict_typedef, new = _StringDict_new, @@ -759,27 +1043,50 @@ StringDict = struct( def _StringKeyedLabelDict_typedef(): """Builder for attr.string_keyed_label_dict. - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool + ::: + + :::{function} allow_files() -> bool | list[str] + ::: + + :::{function} aspects() -> list[aspect] ::: :::{function} build() -> attr.string_list ::: - :::{field} default - :type: dict[str, Label] + :::{field} cfg + :type: AttrCfg ::: - :::{field} doc - :type: Value[str] + :::{function} default() -> dict[str, Label] | callable ::: - :::{field} mandatory - :type: Value[bool] + :::{function} doc() -> str ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{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) ::: """ @@ -792,18 +1099,35 @@ def _StringKeyedLabelDict_new(**kwargs): 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_list(kwargs, "aspects") + kwargs_set_default_list(kwargs, "providers") # buildifier: disable=uninitialized self = struct( - allow_empty = _kwargs_pop_allow_empty(kwargs), - build = lambda *a, **k: attr.string_list(**common_to_kwargs_nobuilders(self, *a, **k)), - default = kwargs_pop_dict(kwargs, "default"), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + allow_files = kwargs_getter(kwargs, "allow_files"), + 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(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_allow_files = kwargs_setter(kwargs, "allow_files"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), + providers = kwargs_getter(kwargs, "providers"), + aspects = kwargs_getter(kwargs, "aspects"), ) return self +# buildifier: disable=name-conventions StringKeyedLabelDict = struct( TYPEDEF = _StringKeyedLabelDict_typedef, new = _StringKeyedLabelDict_new, @@ -812,8 +1136,7 @@ StringKeyedLabelDict = struct( def _StringList_typedef(): """Builder for `attr.string_list` - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{function} build() -> attr.string_list @@ -823,16 +1146,22 @@ def _StringList_typedef(): :type: Value[list[str] | configuration_field] ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} mandatory - :type: Value[bool] + :::{function} mandatory() -> bool ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} set_allow_empty(v: bool) + ::: + + :::{function} set_doc(v: str) + ::: + + :::{function} set_mandatory(v: bool) ::: """ @@ -845,18 +1174,27 @@ def _StringList_new(**kwargs): 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_pop_allow_empty(kwargs), - build = lambda: attr.string_list(**common_to_kwargs_nobuilders(self)), - default = Value.kwargs(kwargs, "default", []), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + build = lambda: attr.string_list(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) return self +# buildifier: disable=name-conventions StringList = struct( TYPEDEF = _StringList_typedef, new = _StringList_new, @@ -865,27 +1203,31 @@ StringList = struct( def _StringListDict_typedef(): """Builder for attr.string_list_dict. - :::{field} allow_empty - :type: Value[bool] + :::{function} allow_empty() -> bool ::: :::{function} build() -> attr.string_list ::: - :::{field} default - :type: dict[str, list[str]] + :::{function} default() -> dict[str, list[str]] + ::: + + :::{function} doc() -> str + ::: + + :::{function} mandatory() -> bool + ::: + + :::{include} /_includes/field_kwargs_doc.md ::: - :::{field} doc - :type: Value[str] + :::{function} set_allow_empty(v: bool) ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_doc(v: str) ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{function} set_mandatory(v: bool) ::: """ @@ -898,24 +1240,34 @@ def _StringListDict_new(**kwargs): 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_pop_allow_empty(kwargs), - build = lambda: attr.string_list(**common_to_kwargs_nobuilders(self)), - default = kwargs_pop_dict(kwargs, "default"), - doc = kwargs_pop_doc(kwargs), - extra_kwargs = kwargs, - mandatory = kwargs_pop_mandatory(kwargs), + allow_empty = kwargs_getter(kwargs, "allow_empty"), + build = lambda: attr.string_list_dict(**self.kwargs), + default = kwargs_getter(kwargs, "default"), + doc = kwargs_getter(kwargs, "doc"), + kwargs = kwargs, + mandatory = kwargs_getter(kwargs, "mandatory"), + set_allow_empty = kwargs_setter(kwargs, "allow_empty"), + set_default = kwargs_setter(kwargs, "default"), + set_doc = kwargs_setter(kwargs, "doc"), + set_mandatory = kwargs_setter(kwargs, "mandatory"), ) 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, diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index b96ac365b7..b57e275406 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -147,45 +147,6 @@ PycCollectionAttr = enum( is_pyc_collection_enabled = _pyc_collection_attr_is_pyc_collection_enabled, ) -def create_stamp_attr(**kwargs): - return { - "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. -""", - **kwargs - ), - } - -def create_srcs_attr(*, mandatory): - fail("hit") - -SRCS_VERSION_ALL_VALUES = [] ##["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"] - -def create_srcs_version_attr(values): - fail("hit") - return { - "srcs_version": attr.string( - default = "PY2AND3", - values = values, - doc = "Defunct, unused, does nothing.", - ), - } - def copy_common_binary_kwargs(kwargs): return { key: kwargs[key] @@ -253,13 +214,13 @@ COMMON_ATTRS = dicts.add( # buildifier: disable=attr-licenses { # NOTE: This attribute is deprecated and slated for removal. - ##"distribs": attr.string_list(), + "distribs": attr.string_list(), # TODO(b/148103851): This attribute is deprecated and slated for # removal. # NOTE: The license attribute is missing in some Java integration tests, # so fallback to a regular string_list for that case. # buildifier: disable=attr-license - ##"licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), + "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), }, ) @@ -397,8 +358,6 @@ as part of a runnable program (packaging rules may include them, however). """, allow_files = True, ), - # Required attribute, but details vary by rule. - # Use create_srcs_attr to create one. "srcs": lambda: attrb.LabelList( # Google builds change the set of allowed files. allow_files = SRCS_ATTR_ALLOW_FILES, @@ -411,10 +370,6 @@ includes all your checked-in code and may include generated source files. The files that may be needed at run time belong in `data`. """, ), - # NOTE: In Google, this attribute is deprecated, and can only - # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute - # has a separate story. - ##"srcs_version": None, "srcs_version": lambda: attrb.String( doc = "Defunct, unused, does nothing.", ), @@ -466,20 +421,39 @@ Specifies additional environment variables to set when the target is executed by `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, + ), }, ) -# 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 = dicts.add( - 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), - { + 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 @@ -488,6 +462,7 @@ Specifies additional environment variables to inherit from the external 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": lambda: attrb.LabelList( default = [ @@ -498,16 +473,17 @@ environment when the test is executed by bazel test. "@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 = dicts.add( - 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_util.bzl b/python/private/builders_util.bzl index 3bf92f360f..b34c431c0f 100644 --- a/python/private/builders_util.bzl +++ b/python/private/builders_util.bzl @@ -12,309 +12,89 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@bazel_skylib//lib:types.bzl", "types") - -def kwargs_pop_dict(kwargs, key): - """Get a dict value for a kwargs key. - - """ - existing = kwargs.pop(key, None) - if existing == None: - return {} - else: - return { - k: v() if types.is_function(v) else v - for k, v in existing.items() - } - -def kwargs_pop_list(kwargs, key): - existing = kwargs.pop(key, None) - if existing == None: - return [] - else: - return [ - v() if types.is_function(v) else v - for v in existing - ] - -def kwargs_pop_doc(kwargs): - return _Value_kwargs(kwargs, "doc", "") - -def kwargs_pop_mandatory(kwargs): - return _Value_kwargs(kwargs, "mandatory", False) - -def to_kwargs_get_pairs(obj, existing_kwargs): - """Partially converts attributes of `obj` to kwarg values. - - This is not a recursive function. Callers must manually handle: - * Attributes that are lists/dicts of non-primitive values. - * Attributes that are builders. - - Args: - obj: A struct whose attributes to turn into kwarg vales. - existing_kwargs: Existing kwarg values that should are already - computed and this function should ignore. - - Returns: - {type}`list[tuple[str, object | Builder]]` a list of key-value - tuples, where the keys are kwarg names, and the values are - a builder for the final value or the final kwarg value. - """ - ignore_names = {"extra_kwargs": None} - pairs = [] - for name in dir(obj): - if name in ignore_names or name in existing_kwargs: - continue - value = getattr(obj, name) - if types.is_function(value): - continue # Assume it's a method - if _is_value_wrapper(value): - value = value.get() - elif _is_optional(value): - if not value.present(): - continue - else: - value = value.get() - - # NOTE: We can't call value.build() here: it would likely lead to - # recursion. - pairs.append((name, value)) - return pairs - -# To avoid recursion, this function shouldn't call `value.build()`. -# Recall that Bazel identifies recursion based on the (line, column) that -# a function (or lambda) is **defined** at -- the closure of variables -# is ignored. Thus, Bazel's recursion detection can be incidentally -# triggered if X.build() calls helper(), which calls Y.build(), which -# then calls helper() again -- helper() is indirectly recursive. -def common_to_kwargs_nobuilders(self, kwargs = None): - """Convert attributes of `self` to kwargs. - - Args: - self: the object whose attributes to convert. - kwargs: An existing kwargs dict to populate. - - Returns: - {type}`dict[str, object]` A new kwargs dict, or the passed-in `kwargs` - if one was passed in. - """ - if kwargs == None: - kwargs = {} - kwargs.update(self.extra_kwargs) - for name, value in to_kwargs_get_pairs(self, kwargs): - kwargs[name] = value - - return kwargs - -def _Optional_typedef(): - """A wrapper for a re-assignable value that may or may not exist at all. - - This allows structs to have attributes whose values can be re-assigned, - e.g. ints, strings, bools, or values where the presence matters. - - This is like {obj}`Value`, except it supports not having a value specified - at all. This allows entirely omitting an argument when the arguments - are constructed for calling e.g. `rule()` - - :::{function} clear() - ::: - """ - -def _Optional_new(*initial): - """Creates an instance. - - Args: - *initial: Either zero, one, or two positional args to set the - initial value stored for the optional. - - If zero args, then no value is stored. - - If one arg, then the arg is the value stored. - - If two args, then the first arg is a kwargs dict, and the - second arg is a name in kwargs to look for. If the name is - present in kwargs, it is removed from kwargs and its value - stored, otherwise kwargs is unmodified and no value is stored. - - Returns: - {type}`Optional` - """ - if len(initial) > 2: - fail("Only zero, one, or two positional args allowed, but got: {}".format(initial)) - - if len(initial) == 2: - kwargs, name = initial - if name in kwargs: - initial = [kwargs.pop(name)] - else: - initial = [] - else: - initial = list(initial) - - # buildifier: disable=uninitialized - self = struct( - # Length zero when no value; length one when has value. - # NOTE: This name is load bearing: it indicates this is a Value - # object; see _is_optional() - _Optional_value = initial, - present = lambda *a, **k: _Optional_present(self, *a, **k), - clear = lambda: self.Optional_value.clear(), - 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. +"""Utilities for builders.""" - Args: - self: implicitly added - value: the value to set. - """ - if len(self._Optional_value) == 0: - self._Optional_value.append(value) - else: - self._Optional_value[0] = value - -def _Optional_get(self): - """Gets the value of the optional, or error. - - Args: - self: implicitly added +load("@bazel_skylib//lib:types.bzl", "types") - Returns: - The stored value, or error if not set. - """ - if not len(self._Optional_value): - fail("Value not present") - return self._Optional_value[0] +def to_label_maybe(value): + """Converts `value` to a `Label`, maybe. -def _Optional_present(self): - """Tells if a value is present. + 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: - self: implicitly added + value: {type}`str | Label | None | object` the value to turn into a label, + or return as-is. Returns: - {type}`bool` True if the value is set, False if not. - """ - return len(self._Optional_value) > 0 - -def _is_optional(obj): - return hasattr(obj, "_Optional_value") - -Optional = struct( - TYPEDEF = _Optional_typedef, - new = _Optional_new, - get = _Optional_get, - set = _Optional_set, - present = _Optional_present, -) - -def _Value_typedef(): - """A wrapper for a re-assignable value that always has some value. - - This allows structs to have attributes whose values can be re-assigned, - e.g. ints, strings, bools, etc. - - This is similar to Optional, except it will always have *some* value - as a default (e.g. None, empty string, empty list, etc) that is OK to pass - onto the rule(), attribute, etc function. - - :::{function} get() -> object - ::: - """ - -def _Value_new(initial): - # buildifier: disable=uninitialized - self = struct( - # NOTE: This name is load bearing: it indicates this is a Value - # object; see _is_value_wrapper() - _Value_value = [initial], - get = lambda: self._Value_value[0], - set = lambda v: _Value_set(self, v), - ) - return self - -def _Value_kwargs(kwargs, name, default): - if name in kwargs: - initial = kwargs[name] - else: - initial = default - return _Value_new(initial) - -def _Value_set(self, v): - """Sets the value. - - Args: - v: the value to set. + {type}`Label | input_value` """ - self._Value_value[0] = v - -def _is_value_wrapper(obj): - return hasattr(obj, "_Value_value") - -Value = struct( - TYPEDEF = _Value_typedef, - new = _Value_new, - kwargs = _Value_kwargs, - set = _Value_set, -) - -def _UniqueList_typedef(): - """A mutable list of unique values. - - Value are kept in insertion order. - - :::{function} update(*others) -> None - ::: + 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 - :::{function} build() -> list - """ +def kwargs_set_default_list(kwargs, key): + """Normalizes None/missing to list.""" + existing = kwargs.get(key) + if existing == None: + kwargs[key] = [] -def _UniqueList_new(kwargs, name): - """Builder for list of unique values. +def kwargs_set_default_dict(kwargs, key): + """Normalizes None/missing to list.""" + existing = kwargs.get(key) + if existing == None: + kwargs[key] = {} - Args: - kwargs: {type}`dict[str, Any]` kwargs to search for `name` - name: {type}`str` A key in `kwargs` to initialize the value - to. If present, kwargs will be modified in place. - initial: {type}`list | None` The initial values. +def kwargs_set_default_doc(kwargs): + """Sets the `doc` arg default.""" + existing = kwargs.get("doc") + if existing == None: + kwargs["doc"] = "" - Returns: - {type}`UniqueList` - """ +def kwargs_set_default_mandatory(kwargs): + """Sets `False` as the `mandatory` arg default.""" + existing = kwargs.get("mandatory") + if existing == None: + kwargs["mandatory"] = False - # TODO - Use set builtin instead of dict, when available. - # https://bazel.build/rules/lib/core/set - initial = {v: None for v in kwargs_pop_list(kwargs, name)} +def kwargs_getter(kwargs, key): + """Create a function to get `key` from `kwargs`.""" + return lambda: kwargs.get(key) - # buildifier: disable=uninitialized - self = struct( - _values = initial, - update = lambda *a, **k: _UniqueList_update(self, *a, **k), - build = lambda *a, **k: _UniqueList_build(self, *a, **k), - ) - return self +def kwargs_setter(kwargs, key): + """Create a function to set `key` in `kwargs`.""" -def _UniqueList_build(self): - """Builds the values into a list + def setter(v): + kwargs[key] = v - Returns: - {type}`list` - """ - return self._values.keys() + return setter -def _UniqueList_update(self, *others): - """Adds values to the builder. +def list_add_unique(add_to, others): + """Bulk add values to a list if not already present. Args: - self: implicitly added - *others: {type}`list` values to add to the set. + 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. """ - for other in others: - for value in other: - if value not in self._values: - self._values[value] = None - -UniqueList = struct( - TYPEDEF = _UniqueList_typedef, - new = _UniqueList_new, -) + 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 8b0ce7486c..48e2653ebb 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -208,57 +208,6 @@ def create_executable_result_struct(*, extra_files_to_build, output_groups, extr 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. - """ - - # todo: probably remove this entirely? is kind of annoying logic to have. - result = {} - for other in attr_dicts: - result.update(other) - return 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 cdafced216..0e1912cf0c 100644 --- a/python/private/py_binary_rule.bzl +++ b/python/private/py_binary_rule.bzl @@ -33,7 +33,6 @@ def create_binary_rule_builder(): executable = True, ) builder.attrs.update(AGNOSTIC_BINARY_ATTRS) - builder.attrs.get("srcs").doc.set("asdf") return builder py_binary = create_binary_rule_builder().build() diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index acd57b1b13..f85f242bba 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -69,6 +69,7 @@ load( load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", + "TARGET_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE", ) @@ -183,9 +184,9 @@ accepting arbitrary Python versions. # TODO: This appears to be vestigial. It's only added because # GraphlessQueryTest.testLabelsOperator relies on it to test for # query behavior of implicit dependencies. - ##"_py_toolchain_type": attr.label( - ## default = TARGET_TOOLCHAIN_TYPE, - ##), + "_py_toolchain_type": attr.label( + default = TARGET_TOOLCHAIN_TYPE, + ), "_python_version_flag": lambda: attrb.Label( default = "//python/config_settings:python_version", ), @@ -209,9 +210,6 @@ accepting arbitrary Python versions. 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): @@ -1743,9 +1741,7 @@ def create_executable_rule_builder(implementation, **kwargs): builder = ruleb.Rule( implementation = implementation, attrs = EXECUTABLE_ATTRS, - # todo: create builder for REQUIRED_EXEC_GROUPS, but keep the - # existing plain dict for now (Google uses it) - exec_groups = REQUIRED_EXEC_GROUP_BUILDERS, + exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), # Mutable copy fragments = ["py", "bazel_py"], provides = [PyExecutableInfo], toolchains = [ @@ -1760,7 +1756,7 @@ def create_executable_rule_builder(implementation, **kwargs): ), **kwargs ) - builder.attrs.get("srcs").mandatory.set(True) + builder.attrs.get("srcs").set_mandatory(True) return builder def cc_configure_features( diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index 877cacde2d..b17cc1b9cd 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -96,31 +96,25 @@ custom_foo_binary = create_custom_foo_binary() load("@bazel_skylib//lib:types.bzl", "types") load( ":builders_util.bzl", - "UniqueList", - "Value", - "common_to_kwargs_nobuilders", - "kwargs_pop_dict", - "kwargs_pop_doc", - "kwargs_pop_list", - "to_kwargs_get_pairs", + "kwargs_getter", + "kwargs_set_default_dict", + "kwargs_set_default_doc", + "kwargs_set_default_ignore_none", + "kwargs_set_default_list", + "kwargs_setter", + "list_add_unique", ) def _ExecGroup_typedef(): """Builder for {external:bzl:obj}`exec_group` - :::{field} toolchains - :type: list[ToolchainType] + :::{function} toolchains() -> list[ToolchainType] ::: - :::{field} exec_compatible_with - :type: list[str] + :::{function} exec_compatible_with() -> list[str | Label] ::: - :::{field} extra_kwargs - :type: dict[str, object] - ::: - - :::{function} build() -> exec_group + :::{include} /_includes/field_kwargs_doc.md ::: """ @@ -133,32 +127,64 @@ def _ExecGroup_new(**kwargs): 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_pop_list(kwargs, "toolchains"), - exec_compatible_with = kwargs_pop_list(kwargs, "exec_compatible_with"), - extra_kwargs = kwargs, - build = lambda: exec_group(**common_to_kwargs_nobuilders(self)), + 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 hasattr(v, "build") else v + for v in kwargs["toolchains"] + ] + if kwargs.get("exec_compatible_with"): + kwargs["exec_compatible_with"] = [ + v.build() if hasattr(v, "build") 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()` - :::{field} extra_kwargs - :type: dict[str, object] + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} mandatory() -> bool + ::: + + :::{function} name() -> str | Label | None ::: - :::{field} mandatory - :type: Value[bool] + :::{function} set_name(v: str) ::: - :::{field} name - :type: Value[str | Label | None] + :::{function} set_mandatory(v: bool) ::: """ @@ -166,21 +192,38 @@ def _ToolchainType_new(name = None, **kwargs): """Creates a builder for `config_common.toolchain_type`. Args: - name: {type}`str | Label` the `toolchain_type` target this creates - a dependency to. + 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), - extra_kwargs = kwargs, - mandatory = Value.kwargs(kwargs, "mandatory", True), - name = Value.new(name), + 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` @@ -190,10 +233,11 @@ def _ToolchainType_build(self): Returns: {type}`config_common.toolchain_type` """ - kwargs = common_to_kwargs_nobuilders(self) + 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, @@ -203,58 +247,97 @@ ToolchainType = struct( def _RuleCfg_typedef(): """Wrapper for `rule.cfg` arg. - :::{field} extra_kwargs - :type: dict[str, object] + :::{function} implementation() -> str | callable | None | config.target | config.none ::: - :::{field} inputs - :type: UniqueList[Label] + ::::{function} inputs() -> list[Label] + + :::{seealso} + The {obj}`add_inputs()` and {obj}`update_inputs` methods for adding unique + values. ::: + :::: + + :::{function} outputs() -> list[Label] - :::{field} outputs - :type: UniqueList[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(kwargs): +def _RuleCfg_new(rule_cfg_arg): """Creates a builder for the `rule.cfg` arg. Args: - kwargs: Same args as `rule.cfg` + rule_cfg_arg: {type}`str | dict` The `cfg` arg passed to Rule(). Returns: {type}`RuleCfg` """ - if kwargs == None: - kwargs = {} + 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( - _implementation = [kwargs.pop("implementation", None)], + 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), - extra_kwargs = kwargs, - implementation = lambda: _RuleCfg_implementation(self), - inputs = UniqueList.new(kwargs, "inputs"), - outputs = UniqueList.new(kwargs, "outputs"), - set_implementation = lambda *a, **k: _RuleCfg_set_implementation(self, *a, **k), + 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_set_implementation(self, value): - """Set the implementation method. +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 added. - value: {type}`str | function` a valid `rule.cfg` argument value. + 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._implementation[0] = value + self.update_inputs(inputs) -def _RuleCfg_implementation(self): - """Returns the implementation name or function for the cfg transition. +def _RuleCfg_add_outputs(self, *outputs): + """Adds an output to the list of outputs, if not present already. - Returns: - {type}`str | function` + :::{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. """ - return self._implementation[0] + self.update_outputs(outputs) def _RuleCfg_build(self): """Builds the rule cfg into the value rule.cfg arg value. @@ -262,26 +345,61 @@ def _RuleCfg_build(self): Returns: {type}`transition` the transition object to apply to the rule. """ - impl = self._implementation[0] + impl = self._state["implementation"] if impl == "target" or impl == None: - return config.target() + # 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, - inputs = self.inputs.build(), - outputs = self.outputs.build(), + # Transitions only accept unique lists of strings. + inputs = {str(v): None for v in self._state.get("inputs")}.keys(), + outputs = {str(v): None for v in self._state.get("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, - implementation = _RuleCfg_implementation, - set_implementation = _RuleCfg_set_implementation, + # 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(): @@ -295,73 +413,88 @@ def _Rule_typedef(): :type: RuleCfg ::: - :::{field} doc - :type: Value[str] + :::{function} doc() -> str ::: - :::{field} exec_groups - :type: dict[str, ExecGroup] + :::{function} exec_groups() -> dict[str, ExecGroup] ::: - :::{field} executable - :type: Value[bool] + :::{function} executable() -> bool ::: - :::{field} extra_kwargs - :type: dict[str, Any] + :::{include} /_includes/field_kwargs_doc.md + ::: + + :::{function} fragments() -> list[str] + ::: + + :::{function} implementation() -> callable | None + ::: - Additional keyword arguments to use when constructing the rule. Their - values have precedence when creating the rule kwargs. This is, essentially, - an escape hatch for manually overriding or inserting values into - the args passed to `rule()`. + :::{function} provides() -> list[provider | list[provider]] ::: - :::{field} fragments - :type: list[str] + :::{function} set_doc(v: str) ::: - :::{field} implementation - :type: Value[callable | None] + :::{function} set_executable(v: bool) ::: - :::{field} provides - :type: list[Provider | list[Provider]] + :::{function} set_implementation(v: callable) ::: - :::{field} test - :type: Value[bool] + :::{function} set_test(v: bool) ::: - :::{field} toolchains - :type: list[ToolchainType] + :::{function} test() -> bool + ::: + + :::{function} toolchains() -> list[ToolchainType] ::: """ -def _Rule_new(implementation = None, **kwargs): +def _Rule_new(**kwargs): """Builder for creating rules. Args: - implementation: {type}`callable` The rule implementation function. **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)), - cfg = _RuleCfg_new(kwargs.pop("cfg", None)), - doc = kwargs_pop_doc(kwargs), - exec_groups = kwargs_pop_dict(kwargs, "exec_groups"), - executable = Value.kwargs(kwargs, "executable", False), - fragments = kwargs_pop_list(kwargs, "fragments"), - implementation = Value.new(implementation), - extra_kwargs = kwargs, - provides = kwargs_pop_list(kwargs, "provides"), - test = Value.kwargs(kwargs, "test", False), - toolchains = kwargs_pop_list(kwargs, "toolchains"), build = lambda *a, **k: _Rule_build(self, *a, **k), - to_kwargs = lambda *a, **k: _Rule_to_kwargs(self, *a, **k), + cfg = _RuleCfg_new(kwargs.pop("cfg", None)), + doc = kwargs_getter(kwargs, "doc"), + 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(kwargs, "doc"), + 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 @@ -379,7 +512,18 @@ def _Rule_build(self, debug = ""): if debug: lines = ["=" * 80, "rule kwargs: {}:".format(debug)] for k, v in sorted(kwargs.items()): - lines.append(" {}={}".format(k, v)) + 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) @@ -390,32 +534,29 @@ def _Rule_to_kwargs(self): kwarg values in case callers want to manually change them. Args: - self: implicitly added + self: implicitly added. Returns: {type}`dict` """ - - kwargs = dict(self.extra_kwargs) - if "exec_groups" not in kwargs: - for k, v in self.exec_groups.items(): - if not hasattr(v, "build"): - fail("bad execgroup", k, v) + kwargs = dict(self.kwargs) + if "exec_groups" in kwargs: kwargs["exec_groups"] = { - k: v.build() - for k, v in self.exec_groups.items() + k: v.build() if hasattr(v, "build") else v + for k, v in kwargs["exec_groups"].items() } - if "toolchains" not in kwargs: + if "toolchains" in kwargs: kwargs["toolchains"] = [ - v.build() - for v in self.toolchains + v.build() if hasattr(v, "build") else v + for v in kwargs["toolchains"] ] - - for name, value in to_kwargs_get_pairs(self, kwargs): - value = value.build() if hasattr(value, "build") else value - kwargs[name] = value + 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, @@ -426,7 +567,7 @@ Rule = struct( def _AttrsDict_typedef(): """Builder for the dictionary of rule attributes. - :::{field} values + :::{field} map :type: dict[str, AttributeBuilder] The underlying dict of attributes. Directly accessible so that regular @@ -434,11 +575,15 @@ def _AttrsDict_typedef(): ::: :::{function} get(key, default=None) - Get an entry from the dict. Convenience wrapper for `.values.get(...)` + 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 `.values.items()` + Returns a list of key-value tuples. Convenience wrapper for `.map.items()` + ::: + + :::{function} pop(key, default) -> object + Removes a key from the attr dict ::: """ @@ -452,21 +597,43 @@ def _AttrsDict_new(initial): Returns: {type}`AttrsDict` """ + + # buildifier: disable=uninitialized self = struct( - values = {}, - update = lambda *a, **k: _AttrsDict_update(self, *a, **k), - get = lambda *a, **k: self.values.get(*a, **k), - items = lambda: self.values.items(), + # 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 @@ -476,9 +643,9 @@ def _AttrsDict_update(self, other): for k, v in other.items(): # Handle factory functions that create builders if types.is_function(v): - self.values[k] = v() + self.map[k] = v() else: - self.values[k] = v + self.map[k] = v def _AttrsDict_build(self): """Build an attribute dict for passing to `rule()`. @@ -487,10 +654,11 @@ def _AttrsDict_build(self): {type}`dict[str, attribute]` where the values are `attr.XXX` objects """ attrs = {} - for k, v in self.values.items(): + for k, v in self.map.items(): attrs[k] = v.build() if hasattr(v, "build") else v return attrs +# buildifier: disable=name-conventions AttrsDict = struct( TYPEDEF = _AttrsDict_typedef, new = _AttrsDict_new, diff --git a/tests/builders/BUILD.bazel b/tests/builders/BUILD.bazel index c79381ff28..f963cb0131 100644 --- a/tests/builders/BUILD.bazel +++ b/tests/builders/BUILD.bazel @@ -21,3 +21,33 @@ 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 index 8024ebd59f..58557cd633 100644 --- a/tests/builders/attr_builders_tests.bzl +++ b/tests/builders/attr_builders_tests.bzl @@ -12,20 +12,30 @@ # 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", "subjects", "truth") -load("@rules_testing//lib:util.bzl", rt_util = "util") +load("@rules_testing//lib:truth.bzl", "truth") load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility -_tests = [] +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") -objs = {} +_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( @@ -34,6 +44,10 @@ def _report_failures(name, env): 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( @@ -47,35 +61,405 @@ def _loading_phase_expect(test_name): ) return env, truth.expect(env) -def _test_bool_defaults(name): +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.get()).equals("") - expect.that_bool(subject.default.get()).equals(False) - expect.that_bool(subject.mandatory.get()).equals(False) - expect.that_dict(subject.extra_kwargs).contains_exactly({}) + 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") - expect.that_str(str(subject.build())).contains("attr.bool") _report_failures(name, env) -_tests.append(_test_bool_defaults) +_tests.append(_test_bool) -def _test_bool_mutable(name): - subject = attrb.Bool() - subject.default.set(True) - subject.mandatory.set(True) - subject.doc.set("doc") - subject.extra_kwargs["extra"] = "value" +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) - expect.that_str(subject.doc.get()).equals("doc") - expect.that_bool(subject.default.get()).equals(True) - expect.that_bool(subject.mandatory.get()).equals(True) - expect.that_dict(subject.extra_kwargs).contains_exactly({"extra": "value"}) + + 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_bool_mutable) +_tests.append(_test_string_list_dict) def attr_builders_test_suite(name): test_suite( diff --git a/tests/builders/rule_builders_tests.bzl b/tests/builders/rule_builders_tests.bzl index 3a72757573..9a91ceb062 100644 --- a/tests/builders/rule_builders_tests.bzl +++ b/tests/builders/rule_builders_tests.bzl @@ -12,46 +12,39 @@ # 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:truth.bzl", "subjects") -load("@rules_testing//lib:util.bzl", rt_util = "util") +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 -BananaInfo = provider() - -def _banana_impl(ctx): - return [BananaInfo( - color = ctx.attr.color, - flavors = ctx.attr.flavors, - organic = ctx.attr.organic, - size = ctx.attr.size, - origin = ctx.attr.origin, - fertilizers = ctx.attr.fertilizers, - xx = mybool, - )] - -banana = ruleb.Rule( - implementation = _banana_impl, +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"), - "flavors": attrb.StringList(), - "organic": lambda: attrb.Bool(), - "size": lambda: attrb.Int(default = 10), - "origin": lambda: attrb.Label(), "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() -_tests = [] - -mybool = attrb.Bool() - -def _test_basic_rule(name): - banana( +def _test_fruit_rule(name): + fruit( name = name + "_subject", flavors = ["spicy", "sweet"], organic = True, @@ -66,21 +59,21 @@ def _test_basic_rule(name): analysis_test( name = name, target = name + "_subject", - impl = _test_basic_rule_impl, + impl = _test_fruit_rule_impl, ) -def _test_basic_rule_impl(env, target): - info = target[BananaInfo] - env.expect.that_str(info.color).equals("yellow") - env.expect.that_collection(info.flavors).contains_exactly(["spicy", "sweet"]) - env.expect.that_bool(info.organic).equals(True) - env.expect.that_int(info.size).equals(5) +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(info.origin).label().equals(Label("//python/private:sentinel")) + env.expect.that_target(attrs.origin).label().equals(Label("//python/private:sentinel")) - env.expect.that_collection(info.fertilizers).transform( + env.expect.that_collection(attrs.fertilizers).transform( desc = "target.label", map_each = lambda t: t.label, ).contains_exactly([ @@ -88,10 +81,176 @@ def _test_basic_rule_impl(env, target): Label(":phosphorus.txt"), ]) -_tests.append(_test_basic_rule) +_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/scratch/BUILD.bazel b/tests/scratch/BUILD.bazel deleted file mode 100644 index a017ce2453..0000000000 --- a/tests/scratch/BUILD.bazel +++ /dev/null @@ -1,3 +0,0 @@ -load(":defs1.bzl", "defs1") - -defs1() diff --git a/tests/scratch/defs1.bzl b/tests/scratch/defs1.bzl deleted file mode 100644 index 863eafdef9..0000000000 --- a/tests/scratch/defs1.bzl +++ /dev/null @@ -1,63 +0,0 @@ -def recursive_build(top): - top_res = {} - - def store_final(nv): - top_res["FINAL"] = nv - - stack = [(top.build, store_final)] - for _ in range(10000): - if not stack: - break - f, store = stack.pop() - f(stack, store) - print("topres=", top_res) - -def Builder(**kwargs): - self = struct( - kwargs = {} | kwargs, - build = lambda *a, **k: _build(self, *a, **k), - ) - return self - -def ListBuilder(*args): - self = struct( - values = list(args), - build = lambda *a, **k: _build_list(self, *a, **k), - ) - return self - -def _build(self, stack, store_result): - result = {} - for k, v in self.kwargs.items(): - if hasattr(v, "build"): - stack.append((v.build, (lambda nv, k = k: _set(result, k, nv)))) - else: - result[k] = v - - store_result(result) - -def _build_list(self, stack, store_result): - list_result = [] - for v in self.values: - if hasattr(v, "build"): - stack.append(v.build, lambda nv: list_result.append(nv)) - else: - list_result.append(v) - store_result(list_result) - -def _set(o, k, v): - o[k] = v - -def defs1(): - top = Builder( - a = Builder( - a1 = True, - ), - b = Builder( - b1 = 2, - b2 = ListBuilder(1, 2, 3), - ), - ) - - todo = [] - recursive_build(top) diff --git a/tests/scratch/defs2.bzl b/tests/scratch/defs2.bzl deleted file mode 100644 index 31eab4c652..0000000000 --- a/tests/scratch/defs2.bzl +++ /dev/null @@ -1,15 +0,0 @@ -def AttrBuilder(values): - return struct( - values = values, - ) - -def Attr(builder_factory): - return struct( - built = builder_factory(), - to_builder = lambda: builder_factory(), - ) - -NEW_THING_BUILDER = lambda: AttrBuilder(values = ["asdf"]) -THING = Attr(NEW_THING_BUILDER) - -D = {"x": None} diff --git a/tests/support/empty_toolchain/BUILD.bazel b/tests/support/empty_toolchain/BUILD.bazel new file mode 100644 index 0000000000..cab5f800ec --- /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 0000000000..e2839283c7 --- /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 df6cbc990d..c5e398483e 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -70,14 +70,14 @@ toolchain. "venvs_use_declare_symlink": attrb.String(), } -def _create_reconfig_rule(builder, is_bin = False): +def _create_reconfig_rule(builder): builder.attrs.update(_RECONFIG_ATTRS) base_cfg_impl = builder.cfg.implementation() builder.cfg.set_implementation(lambda *args: _perform_transition_impl(base_impl = base_cfg_impl, *args)) - builder.cfg.inputs.update(_RECONFIG_INPUTS) - builder.cfg.outputs.update(_RECONFIG_OUTPUTS) - return builder.build(debug = "reconfig") + 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(), True) From f63976e4b165504b64365410f5323494a715c213 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 10 Mar 2025 00:28:09 -0700 Subject: [PATCH 10/13] fix bad reconfig call --- tests/support/sh_py_run_test.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index c5e398483e..d1e3b8e9c8 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -79,7 +79,7 @@ def _create_reconfig_rule(builder): builder.cfg.update_outputs(_RECONFIG_OUTPUTS) return builder.build() -_py_reconfig_binary = _create_reconfig_rule(create_binary_rule_builder(), True) +_py_reconfig_binary = _create_reconfig_rule(create_binary_rule_builder()) _py_reconfig_test = _create_reconfig_rule(create_test_rule_builder()) From 65a3b9fa1b7a733c69ed37d0884bf5c00cf63ef7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 10 Mar 2025 11:37:20 -0700 Subject: [PATCH 11/13] format build file --- python/private/BUILD.bazel | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index d61bb6a15e..b7e52a35aa 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -110,8 +110,6 @@ bzl_library( ], ) - - bzl_library( name = "bzlmod_enabled_bzl", srcs = ["bzlmod_enabled.bzl"], From c71805d9ef513503c136925f61cb4aa5f92088a2 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 13 Mar 2025 11:16:04 -0700 Subject: [PATCH 12/13] address review comments --- python/private/attr_builders.bzl | 442 ++++++++++++++++++------------- python/private/builders_util.bzl | 18 ++ python/private/rule_builders.bzl | 133 ++++++---- 3 files changed, 353 insertions(+), 240 deletions(-) diff --git a/python/private/attr_builders.bzl b/python/private/attr_builders.bzl index aa68fc0f27..acd1d40394 100644 --- a/python/private/attr_builders.bzl +++ b/python/private/attr_builders.bzl @@ -18,29 +18,102 @@ 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") + existing = kwargs.get(_ALLOW_EMPTY) if existing == None: - kwargs["allow_empty"] = True + 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") + existing = kwargs.get(_ALLOW_FILES) if existing == None: - kwargs["allow_files"] = False + 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() + 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. @@ -50,13 +123,16 @@ def _AttrCfg_typedef(): :::{function} outputs() -> list[Label] ::: - :::{function} which_cfg() -> str + :::{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, @@ -78,16 +154,15 @@ def _AttrCfg_new( {type}`AttrCfg` """ state = { - "inputs": inputs, - "outputs": outputs, - # Value depends on "which" key - # For which=impl, the value is a function or arbitrary object - "value": True, - # str: target, exec, none, or impl - "which": "target", + _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") + kwargs_set_default_list(state, _INPUTS) + kwargs_set_default_list(state, _OUTPUTS) # buildifier: disable=uninitialized self = struct( @@ -96,21 +171,21 @@ def _AttrCfg_new( build = lambda: _AttrCfg_build(self), exec_group = lambda: _AttrCfg_exec_group(self), implementation = lambda: _AttrCfg_implementation(self), - inputs = kwargs_getter(state, "inputs"), + inputs = kwargs_getter(state, _INPUTS), none = lambda: _AttrCfg_none(self), - outputs = kwargs_getter(state, "outputs"), + 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, "which"), + 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 in kwargs: + cfg = kwargs.pop(_CFG) if cfg == "target" or cfg == None: self.set_target() elif cfg == "exec": @@ -136,9 +211,9 @@ def _AttrCfg_from_attr_kwargs_pop(attr_kwargs): Returns: {type}`AttrCfg` """ - cfg = attr_kwargs.pop("cfg", None) + cfg = attr_kwargs.pop(_CFG, None) if not types.is_dict(cfg): - kwargs = {"cfg": cfg} + kwargs = {_CFG: cfg} else: kwargs = cfg return _AttrCfg_new(**kwargs) @@ -150,7 +225,7 @@ def _AttrCfg_implementation(self): {type}`callable | None` the custom transition function to use, if any, or `None` if a different config mode is being used. """ - return self._state["value"] if self._state["which"] == "impl" else None + 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. @@ -158,7 +233,7 @@ def _AttrCfg_none(self): Returns: {type}`bool` True if none cfg is set, False if not. """ - return self._state["value"] if self._state["which"] == "none" else False + 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. @@ -166,7 +241,7 @@ def _AttrCfg_target(self): Returns: {type}`bool` True if target cfg is set, False if not. """ - return self._state["value"] if self._state["which"] == "target" else False + 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. @@ -178,7 +253,7 @@ def _AttrCfg_exec_group(self): {type}`str | None` the name of the exec group to use if any, or `None` if `which_cfg` isn't `exec` """ - return self._state["value"] if self._state["which"] == "exec" else None + 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. @@ -187,13 +262,13 @@ def _AttrCfg_set_implementation(self, impl): self: implicitly added. impl: {type}`callable` a transition implementation function. """ - self._state["which"] = "impl" - self._state["value"] = impl + 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["which"] = "none" - self._state["value"] = True + 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. @@ -202,35 +277,35 @@ def _AttrCfg_set_exec(self, exec_group = None): self: implicitly added. exec_group: {type}`str | None` the exec group name to use, if any. """ - self._state["which"] = "exec" - self._state["value"] = exec_group + 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["which"] = "target" - self._state["value"] = True + self._state[_ATTR_CFG_WHICH] = _WhichCfg.TARGET + self._state[_ATTR_CFG_VALUE] = True def _AttrCfg_build(self): - which = self._state["which"] - value = self._state["value"] + which = self._state[_ATTR_CFG_WHICH] + value = self._state[_ATTR_CFG_VALUE] if which == None: return None - elif which == "target": + elif which == _WhichCfg.TARGET: # config.target is Bazel 8+ if hasattr(config, "target"): return config.target() else: return "target" - elif which == "exec": + elif which == _WhichCfg.EXEC: return config.exec(value) - elif which == "none": + 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(), + 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 @@ -290,7 +365,7 @@ def _Bool_new(**kwargs): Returns: {type}`Bool` """ - kwargs_set_default_ignore_none(kwargs, "default", False) + kwargs_set_default_ignore_none(kwargs, _DEFAULT, False) kwargs_set_default_doc(kwargs) kwargs_set_default_mandatory(kwargs) @@ -298,13 +373,13 @@ def _Bool_new(**kwargs): self = struct( # keep sorted build = lambda: attr.bool(**self.kwargs), - default = kwargs_getter(kwargs, "default"), - doc = kwargs_getter(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -356,22 +431,22 @@ def _Int_new(**kwargs): Returns: {type}`Int` """ - kwargs_set_default_ignore_none(kwargs, "default", 0) + kwargs_set_default_ignore_none(kwargs, _DEFAULT, 0) kwargs_set_default_doc(kwargs) kwargs_set_default_mandatory(kwargs) - kwargs_set_default_list(kwargs, "values") + 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(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - values = kwargs_getter(kwargs, "values"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -421,7 +496,7 @@ def _IntList_new(**kwargs): Returns: {type}`IntList` """ - kwargs_set_default_list(kwargs, "default") + kwargs_set_default_list(kwargs, _DEFAULT) kwargs_set_default_doc(kwargs) kwargs_set_default_mandatory(kwargs) _kwargs_set_default_allow_empty(kwargs) @@ -429,15 +504,15 @@ def _IntList_new(**kwargs): # buildifier: disable=uninitialized self = struct( # keep sorted - allow_empty = kwargs_getter(kwargs, "allow_empty"), + allow_empty = _kwargs_getter_allow_empty(kwargs), build = lambda: attr.int_list(**self.kwargs), - default = kwargs_getter(kwargs, "default"), - doc = kwargs_getter(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -516,34 +591,34 @@ def _Label_new(**kwargs): {type}`Label` """ kwargs_set_default_ignore_none(kwargs, "executable", False) - kwargs_set_default_list(kwargs, "aspects") - kwargs_set_default_list(kwargs, "providers") + _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")) + 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(kwargs, "allow_files"), - allow_single_file = kwargs_getter(kwargs, "allow_single_file"), - aspects = kwargs_getter(kwargs, "aspects"), + 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(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), executable = kwargs_getter(kwargs, "executable"), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - providers = kwargs_getter(kwargs, "providers"), + 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(kwargs, "doc"), + set_default = kwargs_setter(kwargs, _DEFAULT), + set_doc = kwargs_setter_doc(kwargs), set_executable = kwargs_setter(kwargs, "executable"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + set_mandatory = kwargs_setter_mandatory(kwargs), ) return self @@ -558,10 +633,10 @@ def _Label_set_allow_files(self, v): If set to `None`, then `allow_files` is unset. """ if v == None: - self.kwargs.pop("allow_files", None) + self.kwargs.pop(_ALLOW_FILES, None) else: - self.kwargs["allow_files"] = v - self.kwargs.pop("allow_single_file", None) + self.kwargs[_ALLOW_FILES] = v + self.kwargs.pop(_ALLOW_SINGLE_FILE, None) def _Label_add_allow_files(self, *values): """Adds allowed file extensions @@ -572,10 +647,10 @@ def _Label_add_allow_files(self, *values): 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"] + 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): @@ -589,10 +664,10 @@ def _Label_set_allow_single_file(self, v): If set to `None`, then `allow_single_file` is unset. """ if v == None: - self.kwargs.pop("allow_single_file", None) + self.kwargs.pop(_ALLOW_SINGLE_FILE, None) else: - self.kwargs["allow_single_file"] = v - self.kwargs.pop("allow_files", None) + self.kwargs[_ALLOW_SINGLE_FILE] = v + self.kwargs.pop(_ALLOW_FILES, None) # buildifier: disable=name-conventions Label = struct( @@ -658,9 +733,9 @@ def _LabelKeyedStringDict_new(**kwargs): Returns: {type}`LabelKeyedStringDict` """ - kwargs_set_default_ignore_none(kwargs, "default", {}) - kwargs_set_default_list(kwargs, "aspects") - kwargs_set_default_list(kwargs, "providers") + 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) @@ -670,21 +745,21 @@ def _LabelKeyedStringDict_new(**kwargs): self = struct( # keep sorted add_allow_files = lambda *v: _LabelKeyedStringDict_add_allow_files(self, *v), - allow_empty = kwargs_getter(kwargs, "allow_empty"), - allow_files = kwargs_getter(kwargs, "allow_files"), - aspects = kwargs_getter(kwargs, "aspects"), + 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(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - providers = kwargs_getter(kwargs, "providers"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_allow_files = kwargs_setter(kwargs, "allow_files"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -695,9 +770,9 @@ def _LabelKeyedStringDict_add_allow_files(self, *values): 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"] + 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 @@ -769,30 +844,30 @@ def _LabelList_new(**kwargs): _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_list(kwargs, "aspects") - kwargs_set_default_list(kwargs, "default") - kwargs_set_default_list(kwargs, "providers") + 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(kwargs, "allow_empty"), - allow_files = kwargs_getter(kwargs, "allow_files"), - aspects = kwargs_getter(kwargs, "aspects"), + 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(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - providers = kwargs_getter(kwargs, "providers"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_allow_files = kwargs_setter(kwargs, "allow_files"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -840,11 +915,11 @@ def _Output_new(**kwargs): self = struct( # keep sorted build = lambda: attr.output(**self.kwargs), - doc = kwargs_getter(kwargs, "doc"), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + mandatory = kwargs_getter_mandatory(kwargs), + set_doc = kwargs_setter_doc(kwargs), + set_mandatory = kwargs_setter_mandatory(kwargs), ) return self @@ -895,14 +970,14 @@ def _OutputList_new(**kwargs): # buildifier: disable=uninitialized self = struct( - allow_empty = kwargs_getter(kwargs, "allow_empty"), + allow_empty = _kwargs_getter_allow_empty(kwargs), build = lambda: attr.output_list(**self.kwargs), - doc = kwargs_getter(kwargs, "doc"), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -952,22 +1027,22 @@ def _String_new(**kwargs): Returns: {type}`String` """ - kwargs_set_default_ignore_none(kwargs, "default", "") - kwargs_set_default_list(kwargs, "values") + 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(kwargs, "doc"), - mandatory = kwargs_getter(kwargs, "mandatory"), + 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(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -1015,22 +1090,22 @@ def _StringDict_new(**kwargs): Returns: {type}`StringDict` """ - kwargs_set_default_ignore_none(kwargs, "default", {}) + 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(kwargs, "allow_empty"), + allow_empty = _kwargs_getter_allow_empty(kwargs), build = lambda: attr.string_dict(**self.kwargs), - default = kwargs_getter(kwargs, "default"), - doc = kwargs_getter(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -1099,31 +1174,31 @@ def _StringKeyedLabelDict_new(**kwargs): Returns: {type}`StringKeyedLabelDict` """ - kwargs_set_default_ignore_none(kwargs, "default", {}) + 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_list(kwargs, "aspects") - kwargs_set_default_list(kwargs, "providers") + _kwargs_set_default_aspects(kwargs) + _kwargs_set_default_providers(kwargs) # buildifier: disable=uninitialized self = struct( - allow_empty = kwargs_getter(kwargs, "allow_empty"), - allow_files = kwargs_getter(kwargs, "allow_files"), + 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(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_allow_files = kwargs_setter(kwargs, "allow_files"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), - providers = kwargs_getter(kwargs, "providers"), - aspects = kwargs_getter(kwargs, "aspects"), + 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 @@ -1174,23 +1249,23 @@ def _StringList_new(**kwargs): Returns: {type}`StringList` """ - kwargs_set_default_ignore_none(kwargs, "default", []) + 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(kwargs, "allow_empty"), + allow_empty = _kwargs_getter_allow_empty(kwargs), build = lambda: attr.string_list(**self.kwargs), - default = kwargs_getter(kwargs, "default"), - doc = kwargs_getter(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -1240,23 +1315,23 @@ def _StringListDict_new(**kwargs): Returns: {type}`StringListDict` """ - kwargs_set_default_ignore_none(kwargs, "default", {}) + 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(kwargs, "allow_empty"), + allow_empty = _kwargs_getter_allow_empty(kwargs), build = lambda: attr.string_list_dict(**self.kwargs), - default = kwargs_getter(kwargs, "default"), - doc = kwargs_getter(kwargs, "doc"), + default = kwargs_getter(kwargs, _DEFAULT), + doc = kwargs_getter_doc(kwargs), kwargs = kwargs, - mandatory = kwargs_getter(kwargs, "mandatory"), - set_allow_empty = kwargs_setter(kwargs, "allow_empty"), - set_default = kwargs_setter(kwargs, "default"), - set_doc = kwargs_setter(kwargs, "doc"), - set_mandatory = kwargs_setter(kwargs, "mandatory"), + 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 @@ -1281,4 +1356,5 @@ attrb = struct( StringKeyedLabelDict = _StringKeyedLabelDict_new, StringList = _StringList_new, StringListDict = _StringListDict_new, + WhichCfg = _WhichCfg, ) diff --git a/python/private/builders_util.bzl b/python/private/builders_util.bzl index b34c431c0f..981f545b02 100644 --- a/python/private/builders_util.bzl +++ b/python/private/builders_util.bzl @@ -84,6 +84,24 @@ def kwargs_setter(kwargs, key): return setter +# todo: use this everywhere +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") + +# todo: use this everywhere +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. diff --git a/python/private/rule_builders.bzl b/python/private/rule_builders.bzl index b17cc1b9cd..6d9fb3f964 100644 --- a/python/private/rule_builders.bzl +++ b/python/private/rule_builders.bzl @@ -97,14 +97,33 @@ 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` @@ -127,16 +146,16 @@ def _ExecGroup_new(**kwargs): Returns: {type}`ExecGroup` """ - kwargs_set_default_list(kwargs, "toolchains") - kwargs_set_default_list(kwargs, "exec_compatible_with") + 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) + 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"), + toolchains = kwargs_getter(kwargs, _TOOLCHAINS), + exec_compatible_with = kwargs_getter(kwargs, _EXEC_COMPATIBLE_WITH), kwargs = kwargs, build = lambda: _ExecGroup_build(self), ) @@ -150,15 +169,15 @@ def _ExecGroup_maybe_from(obj): def _ExecGroup_build(self): kwargs = dict(self.kwargs) - if kwargs.get("toolchains"): - kwargs["toolchains"] = [ - v.build() if hasattr(v, "build") else v - for v in kwargs["toolchains"] + 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 hasattr(v, "build") else v - for v in kwargs["exec_compatible_with"] + 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) @@ -276,7 +295,7 @@ def _RuleCfg_new(rule_cfg_arg): """Creates a builder for the `rule.cfg` arg. Args: - rule_cfg_arg: {type}`str | dict` The `cfg` arg passed to Rule(). + rule_cfg_arg: {type}`str | dict | None` The `cfg` arg passed to Rule(). Returns: {type}`RuleCfg` @@ -287,10 +306,10 @@ def _RuleCfg_new(rule_cfg_arg): else: # Assume its a string, config.target, config.none, or other # valid object. - state["implementation"] = rule_cfg_arg + state[_IMPLEMENTATION] = rule_cfg_arg - kwargs_set_default_list(state, "inputs") - kwargs_set_default_list(state, "outputs") + kwargs_set_default_list(state, _INPUTS) + kwargs_set_default_list(state, _OUTPUTS) # buildifier: disable=uninitialized self = struct( @@ -298,10 +317,10 @@ def _RuleCfg_new(rule_cfg_arg): 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"), + 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), ) @@ -345,7 +364,7 @@ def _RuleCfg_build(self): Returns: {type}`transition` the transition object to apply to the rule. """ - impl = self._state["implementation"] + impl = self._state[_IMPLEMENTATION] if impl == "target" or impl == None: # config.target is Bazel 8+ if hasattr(config, "target"): @@ -358,8 +377,8 @@ def _RuleCfg_build(self): return transition( implementation = impl, # Transitions only accept unique lists of strings. - inputs = {str(v): None for v in self._state.get("inputs")}.keys(), - outputs = {str(v): None for v in self._state.get("outputs")}.keys(), + 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 @@ -376,7 +395,7 @@ def _RuleCfg_update_inputs(self, *others): `Label`, not `str`, should be passed to ensure different apparent labels can be properly de-duplicated. """ - list_add_unique(self._state["inputs"], others) + list_add_unique(self._state[_INPUTS], others) def _RuleCfg_update_outputs(self, *others): """Add a collection of values to outputs. @@ -388,7 +407,7 @@ def _RuleCfg_update_outputs(self, *others): `Label`, not `str`, should be passed to ensure different apparent labels can be properly de-duplicated. """ - list_add_unique(self._state["outputs"], others) + list_add_unique(self._state[_OUTPUTS], others) # buildifier: disable=name-conventions RuleCfg = struct( @@ -461,40 +480,40 @@ def _Rule_new(**kwargs): dicts to specify sub-objects instead of the immutable Bazel objects. """ - kwargs.setdefault("implementation", None) + kwargs.setdefault(_IMPLEMENTATION, None) kwargs_set_default_doc(kwargs) - kwargs_set_default_dict(kwargs, "exec_groups") + 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") + kwargs_set_default_list(kwargs, _TOOLCHAINS) - for name, value in kwargs["exec_groups"].items(): - kwargs["exec_groups"][name] = _ExecGroup_maybe_from(value) + 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) + 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)), + 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(kwargs, "doc"), - exec_groups = kwargs_getter(kwargs, "exec_groups"), + 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"), + implementation = kwargs_getter(kwargs, _IMPLEMENTATION), kwargs = kwargs, provides = kwargs_getter(kwargs, "provides"), - set_doc = kwargs_setter(kwargs, "doc"), + set_doc = kwargs_setter_doc(kwargs), set_executable = kwargs_setter(kwargs, "executable"), - set_implementation = kwargs_setter(kwargs, "implementation"), + 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"), + toolchains = kwargs_getter(kwargs, _TOOLCHAINS), ) return self @@ -540,20 +559,20 @@ def _Rule_to_kwargs(self): {type}`dict` """ kwargs = dict(self.kwargs) - if "exec_groups" in kwargs: - kwargs["exec_groups"] = { - k: v.build() if hasattr(v, "build") else v - for k, v in kwargs["exec_groups"].items() + 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 hasattr(v, "build") else v - for v in kwargs["toolchains"] + 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() + 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 @@ -591,8 +610,8 @@ def _AttrsDict_new(initial): """Creates a builder for the `rule.attrs` dict. Args: - initial: {type}`dict[str, callable | AttributeBuilder]` dict of initial - values to populate the attributes dict with. + initial: {type}`dict[str, callable | AttributeBuilder] | None` dict of + initial values to populate the attributes dict with. Returns: {type}`AttrsDict` @@ -655,7 +674,7 @@ def _AttrsDict_build(self): """ attrs = {} for k, v in self.map.items(): - attrs[k] = v.build() if hasattr(v, "build") else v + attrs[k] = v.build() if _is_builder(v) else v return attrs # buildifier: disable=name-conventions From 383a7a23bcee5c289f746ff6e2d2a9b6c43ba76d Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Thu, 13 Mar 2025 16:05:39 -0700 Subject: [PATCH 13/13] remove todo --- python/private/builders_util.bzl | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/private/builders_util.bzl b/python/private/builders_util.bzl index 981f545b02..139084f79a 100644 --- a/python/private/builders_util.bzl +++ b/python/private/builders_util.bzl @@ -84,7 +84,6 @@ def kwargs_setter(kwargs, key): return setter -# todo: use this everywhere def kwargs_getter_doc(kwargs): """Creates a `kwargs_getter` for the `doc` key.""" return kwargs_getter(kwargs, "doc") @@ -93,7 +92,6 @@ def kwargs_setter_doc(kwargs): """Creates a `kwargs_setter` for the `doc` key.""" return kwargs_setter(kwargs, "doc") -# todo: use this everywhere def kwargs_getter_mandatory(kwargs): """Creates a `kwargs_getter` for the `mandatory` key.""" return kwargs_getter(kwargs, "mandatory")