From 9af766887008b0e2a4edc25e54b2376148deb453 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Fri, 13 Jun 2025 10:12:41 +0100 Subject: [PATCH 1/4] Add `structs.merge` --- docs/structs_doc.md | 25 +++++++++++++++++++++++++ lib/structs.bzl | 16 ++++++++++++++++ tests/structs_tests.bzl | 24 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/docs/structs_doc.md b/docs/structs_doc.md index 00585f55..78db98eb 100755 --- a/docs/structs_doc.md +++ b/docs/structs_doc.md @@ -2,6 +2,31 @@ Skylib module containing functions that operate on structs. + + +## structs.merge + +
+load("@bazel_skylib//lib:structs.bzl", "structs")
+
+structs.merge(first, *rest)
+
+ +Merges multiple `struct` instances together. Later `struct` keys overwrite early `struct` keys. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| first | The initial `struct` to merge keys/values into. | none | +| rest | Other `struct` instances to merge. | none | + +**RETURNS** + +A merged `struct`. + + ## structs.to_dict diff --git a/lib/structs.bzl b/lib/structs.bzl index 78066ad9..20f17b03 100644 --- a/lib/structs.bzl +++ b/lib/structs.bzl @@ -34,6 +34,22 @@ def _to_dict(s): if key != "to_json" and key != "to_proto" } +def _merge(first, *rest): + """Merges multiple `struct` instances together. Later `struct` keys overwrite early `struct` keys. + + Args: + first: The initial `struct` to merge keys/values into. + *rest: Other `struct` instances to merge. + + Returns: + A merged `struct`. + """ + map = _to_dict(first) + for r in rest: + map |= _to_dict(r) + return struct(**map) + structs = struct( to_dict = _to_dict, + merge = _merge, ) diff --git a/tests/structs_tests.bzl b/tests/structs_tests.bzl index 79de7ad1..2be917ae 100644 --- a/tests/structs_tests.bzl +++ b/tests/structs_tests.bzl @@ -46,9 +46,33 @@ def _add_test(ctx): add_test = unittest.make(_add_test) +def _merge_test(ctx): + """Unit tests for structs.merge.""" + env = unittest.begin(ctx) + + # Fixtures + a = struct(a = 1) + b = struct(b = 2) + c = struct(a = 3) + + # Test one argument + asserts.equals(env, {"a": 1}, structs.to_dict(structs.merge(a))) + + # Test two arguments + asserts.equals(env, {"a": 1}, structs.to_dict(structs.merge(a, a))) + asserts.equals(env, {"a": 1, "b": 2}, structs.to_dict(structs.merge(a, b))) + + # Test overwrite + asserts.equals(env, {"a": 3}, structs.to_dict(structs.merge(a, c))) + + return unittest.end(env) + +merge_test = unittest.make(_merge_test) + def structs_test_suite(): """Creates the test targets and test suite for structs.bzl tests.""" unittest.suite( "structs_tests", add_test, + merge_test, ) From fbe6e26cbc25c539d310d3f2e3438d0b1701aafc Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Fri, 13 Jun 2025 13:50:57 +0100 Subject: [PATCH 2/4] Update `structs.to_dict` test function name Copy/paste error. --- tests/structs_tests.bzl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/structs_tests.bzl b/tests/structs_tests.bzl index 2be917ae..d7d57ff4 100644 --- a/tests/structs_tests.bzl +++ b/tests/structs_tests.bzl @@ -17,8 +17,8 @@ load("//lib:structs.bzl", "structs") load("//lib:unittest.bzl", "asserts", "unittest") -def _add_test(ctx): - """Unit tests for dicts.add.""" +def _to_dict_test(ctx): + """Unit tests for structs.to_dict.""" env = unittest.begin(ctx) # Test zero- and one-argument behavior. @@ -44,7 +44,7 @@ def _add_test(ctx): return unittest.end(env) -add_test = unittest.make(_add_test) +to_dict_test = unittest.make(_to_dict_test) def _merge_test(ctx): """Unit tests for structs.merge.""" @@ -73,6 +73,6 @@ def structs_test_suite(): """Creates the test targets and test suite for structs.bzl tests.""" unittest.suite( "structs_tests", - add_test, + to_dict_test, merge_test, ) From 3746c5207db98b97ded65a23b48be85a33411fb7 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Fri, 13 Jun 2025 14:01:34 +0100 Subject: [PATCH 3/4] Check `struct.{to_json,to_proto}` are built-in functions `to_json`/`to_proto` function have been removed from `structs`. Check that they are built-in functions before eliminating the keys. Tests are gated behind `bazel_features.rules.no_struct_field_denylist` to prevent breakage on old Bazel when running the tests. --- MODULE.bazel | 1 + WORKSPACE | 13 +++++++++++++ lib/structs.bzl | 16 +++++++++++++++- tests/structs_tests.bzl | 22 ++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index fb1a4fd6..ee54e0c0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,6 +21,7 @@ bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True) bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) bazel_dep(name = "rules_cc", version = "0.0.17", dev_dependency = True) bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True) +bazel_dep(name = "bazel_features", version = "1.32.0", dev_dependency = True) # Needed for bazelci and for building distribution tarballs. # If using an unreleased version of bazel_skylib via git_override, apply diff --git a/WORKSPACE b/WORKSPACE index d9d5d66b..a9803261 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -44,6 +44,19 @@ rules_shell_dependencies() rules_shell_toolchains() +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "bazel_features", + sha256 = "07bd2b18764cdee1e0d6ff42c9c0a6111ffcbd0c17f0de38e7f44f1519d1c0cd", + strip_prefix = "bazel_features-1.32.0", + url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.32.0/bazel_features-v1.32.0.tar.gz", +) + +load("@bazel_features//:deps.bzl", "bazel_features_deps") + +bazel_features_deps() + maybe( http_archive, name = "io_bazel_stardoc", diff --git a/lib/structs.bzl b/lib/structs.bzl index 20f17b03..b0335858 100644 --- a/lib/structs.bzl +++ b/lib/structs.bzl @@ -14,6 +14,20 @@ """Skylib module containing functions that operate on structs.""" +_built_in_function = type(str) + +def _is_built_in_function(v): + """Returns True if v is an instance of a built-in function. + + Args: + v: The value whose type should be checked. + + Returns: + True if v is an instance of a built-in function, False otherwise. + """ + + return type(v) == _built_in_function + def _to_dict(s): """Converts a `struct` to a `dict`. @@ -31,7 +45,7 @@ def _to_dict(s): return { key: getattr(s, key) for key in dir(s) - if key != "to_json" and key != "to_proto" + if not ((key == "to_json" or key == "to_proto") and _is_built_in_function(getattr(s, key))) } def _merge(first, *rest): diff --git a/tests/structs_tests.bzl b/tests/structs_tests.bzl index d7d57ff4..8313d32f 100644 --- a/tests/structs_tests.bzl +++ b/tests/structs_tests.bzl @@ -14,9 +14,13 @@ """Unit tests for structs.bzl.""" +load("@bazel_features//:features.bzl", "bazel_features") load("//lib:structs.bzl", "structs") load("//lib:unittest.bzl", "asserts", "unittest") +def _placeholder(): + pass + def _to_dict_test(ctx): """Unit tests for structs.to_dict.""" env = unittest.begin(ctx) @@ -42,6 +46,24 @@ def _to_dict_test(ctx): structs.to_dict(struct(a = 1, b = struct(bb = 1))), ) + # Older Bazel denied creating `struct` with `to_json`/`to_proto` + if not bazel_features.rules.no_struct_field_denylist: + return unittest.end(env) + + # Test `to_json`/`to_proto` values are propagated + asserts.equals( + env, + {"to_json": 1, "to_proto": 2}, + structs.to_dict(struct(to_json = 1, to_proto = 2)), + ) + + # Test `to_json`/`to_proto` functions are propagated + asserts.equals( + env, + {"to_json": _placeholder, "to_proto": _placeholder}, + structs.to_dict(struct(to_json = _placeholder, to_proto = _placeholder)), + ) + return unittest.end(env) to_dict_test = unittest.make(_to_dict_test) From 07485dc0a9883b2941864d858b0020edec598df4 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Fri, 13 Jun 2025 14:01:34 +0100 Subject: [PATCH 4/4] Check `struct.{to_json,to_proto}` are built-in functions `to_json`/`to_proto` function have been removed from `structs`. Check that they are built-in functions before eliminating the keys. Tests are gated behind `bazel_features.rules.no_struct_field_denylist` to prevent breakage on old Bazel when running the tests. --- WORKSPACE | 2 -- 1 file changed, 2 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index a9803261..a2fb9f2f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -44,8 +44,6 @@ rules_shell_dependencies() rules_shell_toolchains() -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - http_archive( name = "bazel_features", sha256 = "07bd2b18764cdee1e0d6ff42c9c0a6111ffcbd0c17f0de38e7f44f1519d1c0cd",