3535load (":int_subject.bzl" , "IntSubject" )
3636load (":matching.bzl" , "matching" )
3737load (":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
3947def _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
359449CollectionSubject = 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)
0 commit comments