Skip to content

Commit

Permalink
Merge pull request #58 from rickeylev:transform
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 546048463
  • Loading branch information
Blaze Rules Copybara committed Jul 6, 2023
2 parents 22cbb53 + f170f48 commit 74f8bd5
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/private/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ bzl_library(
":int_subject_bzl",
":matching_bzl",
":truth_common_bzl",
":util_bzl",
],
)

Expand Down
91 changes: 91 additions & 0 deletions lib/private/collection_subject.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ load(
load(":int_subject.bzl", "IntSubject")
load(":matching.bzl", "matching")
load(":truth_common.bzl", "to_list")
load(":util.bzl", "get_function_name")

def _identity(v):
return v

def _always_true(v):
_ = v # @unused
return True

def _collection_subject_new(
values,
Expand Down Expand Up @@ -75,6 +83,7 @@ def _collection_subject_new(
not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k),
not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k),
offset = lambda *a, **k: _collection_subject_offset(self, *a, **k),
transform = lambda *a, **k: _collection_subject_transform(self, *a, **k),
# keep sorted end
)
self = struct(
Expand Down Expand Up @@ -354,6 +363,87 @@ def _collection_subject_offset(self, offset, factory):
meta = self.meta.derive("offset({})".format(offset)),
)

def _collection_subject_transform(
self,
desc = None,
*,
map_each = None,
loop = None,
filter = None):
"""Transforms a collections's value and returns another CollectionSubject.
This is equivalent to applying a list comprehension over the collection values,
but takes care of propagating context information and wrapping the value
in a `CollectionSubject`.
`transform(map_each=M, loop=L, filter=F)` is equivalent to
`[M(v) for v in L(collection) if F(v)]`.
Args:
self: implicitly added.
desc: (optional [`str`]) a human-friendly description of the transform
for use in error messages. Required when a description can't be
inferred from the other args. The description can be inferred if the
filter arg is a named function (non-lambda) or Matcher object.
map_each: (optional [`callable`]) function to transform an element in
the collection. It takes one positional arg, the loop's
current iteration value, and its return value will be the element's
new value. If not specified, the values from the loop iteration are
returned unchanged.
loop: (optional [`callable`]) function to produce values from the
original collection and whose values are iterated over. It takes one
positional arg, which is the original collection. If not specified,
the original collection values are iterated over.
filter: (optional [`callable`]) function that decides what values are
passed onto `map_each` for inclusion in the final result. It takes
one positional arg, the value to match (which is the current
iteration value before `map_each` is applied), and returns a bool
(True if the value should be included in the result, False if it
should be skipped).
Returns:
[`CollectionSubject`] of the transformed values.
"""
if not desc:
if map_each or loop:
fail("description required when map_each or loop used")

if matching.is_matcher(filter):
desc = "filter=" + filter.desc
else:
func_name = get_function_name(filter)
if func_name == "lambda":
fail("description required: description cannot be " +
"inferred from lambdas. Explicitly specify the " +
"description, use a named function for the filter, " +
"or use a Matcher for the filter.")
else:
desc = "filter={}(...)".format(func_name)

map_each = map_each or _identity
loop = loop or _identity

if filter:
if matching.is_matcher(filter):
filter_func = filter.match
else:
filter_func = filter
else:
filter_func = _always_true

new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)]

return _collection_subject_new(
new_values,
meta = self.meta.derive(
"transform()",
details = ["transform: {}".format(desc)],
),
container_name = self.container_name,
sortable = self.sortable,
element_plural_name = self.element_plural_name,
)

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
CollectionSubject = struct(
Expand All @@ -369,5 +459,6 @@ CollectionSubject = struct(
new = _collection_subject_new,
not_contains_predicate = _collection_subject_not_contains_predicate,
offset = _collection_subject_offset,
transform = _collection_subject_transform,
# keep sorted end
)
5 changes: 3 additions & 2 deletions lib/private/expect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,19 @@ def _expect_that_bool(self, value, expr = "boolean"):
meta = self.meta.derive(expr = expr),
)

def _expect_that_collection(self, collection, expr = "collection"):
def _expect_that_collection(self, collection, expr = "collection", **kwargs):
"""Creates a subject for asserting collections.
Args:
self: implicitly added.
collection: The collection (list or depset) to assert.
expr: ([`str`]) the starting "value of" expression to report in errors.
**kwargs: Additional kwargs to pass onto CollectionSubject.new
Returns:
[`CollectionSubject`] object.
"""
return CollectionSubject.new(collection, self.meta.derive(expr))
return CollectionSubject.new(collection, self.meta.derive(expr), **kwargs)

def _expect_that_depset_of_files(self, depset_files):
"""Creates a subject for asserting a depset of files.
Expand Down
2 changes: 1 addition & 1 deletion lib/private/expect_meta.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def _expect_meta_add_failure(self, problem, actual):
if detail
])
if details:
details = "where...\n" + details
details = "where... (most recent context last)\n" + details
msg = """\
in test: {test}
value of: {expr}
Expand Down
4 changes: 4 additions & 0 deletions lib/private/matching.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ def _match_parts_in_order(string, parts):
return False
return True

def _is_matcher(obj):
return hasattr(obj, "desc") and hasattr(obj, "match")

# For the definition of a `Matcher` object, see `_match_custom`.
matching = struct(
# keep sorted start
Expand All @@ -229,5 +232,6 @@ matching = struct(
str_endswith = _match_str_endswith,
str_matches = _match_str_matches,
str_startswith = _match_str_startswith,
is_matcher = _is_matcher,
# keep sorted end
)
2 changes: 2 additions & 0 deletions lib/private/util.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ def get_test_name_from_function(func):
# have private names. This better allows unused tests to be flagged by
# buildifier (indicating a bug or code to delete)
return func_name.strip("_")

get_function_name = get_test_name_from_function
75 changes: 75 additions & 0 deletions tests/truth_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,81 @@ def _collection_offset_test(env, _target):

_suite.append(collection_offset_test)

def _collection_transform_test(name):
analysis_test(name, impl = _collection_transform_test_impl, target = "truth_tests_helper")

def _collection_transform_test_impl(env, target):
_ = target # @unused
fake_env = _fake_env(env)
starter = truth.expect(fake_env).that_collection(["alan", "bert", "cari"])

actual = starter.transform(
"values that contain a",
filter = lambda v: "a" in v,
)
actual.contains("not-present")
_assert_failure(
fake_env,
[
"transform()",
"0: alan",
"1: cari",
"transform: values that contain a",
],
env = env,
msg = "transform with lambda filter",
)

actual = starter.transform(filter = matching.contains("b"))
actual.contains("not-present")
_assert_failure(
fake_env,
[
"0: bert",
"transform: filter=<contains b>",
],
env = env,
msg = "transform with matcher filter",
)

def contains_c(v):
return "c" in v

actual = starter.transform(filter = contains_c)
actual.contains("not-present")
_assert_failure(
fake_env,
[
"0: cari",
"transform: filter=contains_c(...)",
],
env = env,
msg = "transform with named function filter",
)

actual = starter.transform(
"v.upper(); match even offsets",
map_each = lambda v: "{}-{}".format(v[0], v[1].upper()),
loop = enumerate,
)
actual.contains("not-present")
_assert_failure(
fake_env,
[
"transform()",
"0: 0-ALAN",
"1: 1-BERT",
"2: 2-CARI",
"transform: v.upper(); match even offsets",
],
env = env,
msg = "transform with all args",
)

_end(env, fake_env)

_suite.append(_collection_transform_test)

def execution_info_test(name):
analysis_test(name, impl = _execution_info_test, target = "truth_tests_helper")

Expand Down

0 comments on commit 74f8bd5

Please sign in to comment.