Skip to content

Commit 74f8bd5

Browse files
author
Blaze Rules Copybara
committed
Merge pull request #58 from rickeylev:transform
PiperOrigin-RevId: 546048463
2 parents 22cbb53 + f170f48 commit 74f8bd5

File tree

7 files changed

+177
-3
lines changed

7 files changed

+177
-3
lines changed

lib/private/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ bzl_library(
6363
":int_subject_bzl",
6464
":matching_bzl",
6565
":truth_common_bzl",
66+
":util_bzl",
6667
],
6768
)
6869

lib/private/collection_subject.bzl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ load(
3535
load(":int_subject.bzl", "IntSubject")
3636
load(":matching.bzl", "matching")
3737
load(":truth_common.bzl", "to_list")
38+
load(":util.bzl", "get_function_name")
39+
40+
def _identity(v):
41+
return v
42+
43+
def _always_true(v):
44+
_ = v # @unused
45+
return True
3846

3947
def _collection_subject_new(
4048
values,
@@ -75,6 +83,7 @@ def _collection_subject_new(
7583
not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k),
7684
not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k),
7785
offset = lambda *a, **k: _collection_subject_offset(self, *a, **k),
86+
transform = lambda *a, **k: _collection_subject_transform(self, *a, **k),
7887
# keep sorted end
7988
)
8089
self = struct(
@@ -354,6 +363,87 @@ def _collection_subject_offset(self, offset, factory):
354363
meta = self.meta.derive("offset({})".format(offset)),
355364
)
356365

366+
def _collection_subject_transform(
367+
self,
368+
desc = None,
369+
*,
370+
map_each = None,
371+
loop = None,
372+
filter = None):
373+
"""Transforms a collections's value and returns another CollectionSubject.
374+
375+
This is equivalent to applying a list comprehension over the collection values,
376+
but takes care of propagating context information and wrapping the value
377+
in a `CollectionSubject`.
378+
379+
`transform(map_each=M, loop=L, filter=F)` is equivalent to
380+
`[M(v) for v in L(collection) if F(v)]`.
381+
382+
Args:
383+
self: implicitly added.
384+
desc: (optional [`str`]) a human-friendly description of the transform
385+
for use in error messages. Required when a description can't be
386+
inferred from the other args. The description can be inferred if the
387+
filter arg is a named function (non-lambda) or Matcher object.
388+
map_each: (optional [`callable`]) function to transform an element in
389+
the collection. It takes one positional arg, the loop's
390+
current iteration value, and its return value will be the element's
391+
new value. If not specified, the values from the loop iteration are
392+
returned unchanged.
393+
loop: (optional [`callable`]) function to produce values from the
394+
original collection and whose values are iterated over. It takes one
395+
positional arg, which is the original collection. If not specified,
396+
the original collection values are iterated over.
397+
filter: (optional [`callable`]) function that decides what values are
398+
passed onto `map_each` for inclusion in the final result. It takes
399+
one positional arg, the value to match (which is the current
400+
iteration value before `map_each` is applied), and returns a bool
401+
(True if the value should be included in the result, False if it
402+
should be skipped).
403+
404+
Returns:
405+
[`CollectionSubject`] of the transformed values.
406+
"""
407+
if not desc:
408+
if map_each or loop:
409+
fail("description required when map_each or loop used")
410+
411+
if matching.is_matcher(filter):
412+
desc = "filter=" + filter.desc
413+
else:
414+
func_name = get_function_name(filter)
415+
if func_name == "lambda":
416+
fail("description required: description cannot be " +
417+
"inferred from lambdas. Explicitly specify the " +
418+
"description, use a named function for the filter, " +
419+
"or use a Matcher for the filter.")
420+
else:
421+
desc = "filter={}(...)".format(func_name)
422+
423+
map_each = map_each or _identity
424+
loop = loop or _identity
425+
426+
if filter:
427+
if matching.is_matcher(filter):
428+
filter_func = filter.match
429+
else:
430+
filter_func = filter
431+
else:
432+
filter_func = _always_true
433+
434+
new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)]
435+
436+
return _collection_subject_new(
437+
new_values,
438+
meta = self.meta.derive(
439+
"transform()",
440+
details = ["transform: {}".format(desc)],
441+
),
442+
container_name = self.container_name,
443+
sortable = self.sortable,
444+
element_plural_name = self.element_plural_name,
445+
)
446+
357447
# We use this name so it shows up nice in docs.
358448
# buildifier: disable=name-conventions
359449
CollectionSubject = struct(
@@ -369,5 +459,6 @@ CollectionSubject = struct(
369459
new = _collection_subject_new,
370460
not_contains_predicate = _collection_subject_not_contains_predicate,
371461
offset = _collection_subject_offset,
462+
transform = _collection_subject_transform,
372463
# keep sorted end
373464
)

lib/private/expect.bzl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,19 @@ def _expect_that_bool(self, value, expr = "boolean"):
120120
meta = self.meta.derive(expr = expr),
121121
)
122122

123-
def _expect_that_collection(self, collection, expr = "collection"):
123+
def _expect_that_collection(self, collection, expr = "collection", **kwargs):
124124
"""Creates a subject for asserting collections.
125125
126126
Args:
127127
self: implicitly added.
128128
collection: The collection (list or depset) to assert.
129129
expr: ([`str`]) the starting "value of" expression to report in errors.
130+
**kwargs: Additional kwargs to pass onto CollectionSubject.new
130131
131132
Returns:
132133
[`CollectionSubject`] object.
133134
"""
134-
return CollectionSubject.new(collection, self.meta.derive(expr))
135+
return CollectionSubject.new(collection, self.meta.derive(expr), **kwargs)
135136

136137
def _expect_that_depset_of_files(self, depset_files):
137138
"""Creates a subject for asserting a depset of files.

lib/private/expect_meta.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def _expect_meta_add_failure(self, problem, actual):
234234
if detail
235235
])
236236
if details:
237-
details = "where...\n" + details
237+
details = "where... (most recent context last)\n" + details
238238
msg = """\
239239
in test: {test}
240240
value of: {expr}

lib/private/matching.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ def _match_parts_in_order(string, parts):
214214
return False
215215
return True
216216

217+
def _is_matcher(obj):
218+
return hasattr(obj, "desc") and hasattr(obj, "match")
219+
217220
# For the definition of a `Matcher` object, see `_match_custom`.
218221
matching = struct(
219222
# keep sorted start
@@ -229,5 +232,6 @@ matching = struct(
229232
str_endswith = _match_str_endswith,
230233
str_matches = _match_str_matches,
231234
str_startswith = _match_str_startswith,
235+
is_matcher = _is_matcher,
232236
# keep sorted end
233237
)

lib/private/util.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ def get_test_name_from_function(func):
3131
# have private names. This better allows unused tests to be flagged by
3232
# buildifier (indicating a bug or code to delete)
3333
return func_name.strip("_")
34+
35+
get_function_name = get_test_name_from_function

tests/truth_tests.bzl

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,81 @@ def _collection_offset_test(env, _target):
832832

833833
_suite.append(collection_offset_test)
834834

835+
def _collection_transform_test(name):
836+
analysis_test(name, impl = _collection_transform_test_impl, target = "truth_tests_helper")
837+
838+
def _collection_transform_test_impl(env, target):
839+
_ = target # @unused
840+
fake_env = _fake_env(env)
841+
starter = truth.expect(fake_env).that_collection(["alan", "bert", "cari"])
842+
843+
actual = starter.transform(
844+
"values that contain a",
845+
filter = lambda v: "a" in v,
846+
)
847+
actual.contains("not-present")
848+
_assert_failure(
849+
fake_env,
850+
[
851+
"transform()",
852+
"0: alan",
853+
"1: cari",
854+
"transform: values that contain a",
855+
],
856+
env = env,
857+
msg = "transform with lambda filter",
858+
)
859+
860+
actual = starter.transform(filter = matching.contains("b"))
861+
actual.contains("not-present")
862+
_assert_failure(
863+
fake_env,
864+
[
865+
"0: bert",
866+
"transform: filter=<contains b>",
867+
],
868+
env = env,
869+
msg = "transform with matcher filter",
870+
)
871+
872+
def contains_c(v):
873+
return "c" in v
874+
875+
actual = starter.transform(filter = contains_c)
876+
actual.contains("not-present")
877+
_assert_failure(
878+
fake_env,
879+
[
880+
"0: cari",
881+
"transform: filter=contains_c(...)",
882+
],
883+
env = env,
884+
msg = "transform with named function filter",
885+
)
886+
887+
actual = starter.transform(
888+
"v.upper(); match even offsets",
889+
map_each = lambda v: "{}-{}".format(v[0], v[1].upper()),
890+
loop = enumerate,
891+
)
892+
actual.contains("not-present")
893+
_assert_failure(
894+
fake_env,
895+
[
896+
"transform()",
897+
"0: 0-ALAN",
898+
"1: 1-BERT",
899+
"2: 2-CARI",
900+
"transform: v.upper(); match even offsets",
901+
],
902+
env = env,
903+
msg = "transform with all args",
904+
)
905+
906+
_end(env, fake_env)
907+
908+
_suite.append(_collection_transform_test)
909+
835910
def execution_info_test(name):
836911
analysis_test(name, impl = _execution_info_test, target = "truth_tests_helper")
837912

0 commit comments

Comments
 (0)