Skip to content

Commit 645b512

Browse files
authored
Merge pull request #6 from ringoldsdev/feat/20250722/tap-accepts-a-transformer
feat: tap accepts a transformer
2 parents 4c05994 + 12ba447 commit 645b512

File tree

2 files changed

+100
-5
lines changed

2 files changed

+100
-5
lines changed

laygo/transformers/transformer.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,54 @@ def flatten[T](
123123
"""Flattens nested lists; the context is passed through the operation."""
124124
return self._pipe(lambda chunk, ctx: [item for sublist in chunk for item in sublist]) # type: ignore
125125

126-
def tap(self, function: PipelineFunction[Out, Any]) -> "Transformer[In, Out]":
127-
"""Applies a side-effect function without modifying the data."""
126+
@overload
127+
def tap(self, arg: "Transformer[Out, Any]") -> "Transformer[In, Out]": ...
128128

129-
if is_context_aware(function):
130-
return self._pipe(lambda chunk, ctx: [x for x in chunk if function(x, ctx) or True])
129+
@overload
130+
def tap(self, arg: PipelineFunction[Out, Any]) -> "Transformer[In, Out]": ...
131+
132+
def tap(
133+
self,
134+
arg: Union["Transformer[Out, Any]", PipelineFunction[Out, Any]],
135+
) -> "Transformer[In, Out]":
136+
"""
137+
Applies a side-effect without modifying the main data stream.
138+
139+
This method can be used in two ways:
140+
1. With a `Transformer`: Applies a sub-pipeline to each chunk for side-effects
141+
(e.g., logging a chunk), discarding the sub-pipeline's output.
142+
2. With a `function`: Applies a function to each element individually for
143+
side-effects (e.g., printing an item).
144+
145+
Args:
146+
arg: A `Transformer` instance or a function to be applied for side-effects.
147+
148+
Returns:
149+
The transformer instance for method chaining.
150+
"""
151+
match arg:
152+
# Case 1: The argument is another Transformer
153+
case Transformer() as tapped_transformer:
154+
tapped_func = tapped_transformer.transformer
155+
156+
def operation(chunk: list[Out], ctx: PipelineContext) -> list[Out]:
157+
# Execute the tapped transformer's logic on the chunk for side-effects.
158+
_ = tapped_func(chunk, ctx)
159+
# Return the original chunk to continue the main pipeline.
160+
return chunk
161+
162+
return self._pipe(operation)
163+
164+
# Case 2: The argument is a callable function
165+
case function if callable(function):
166+
if is_context_aware(function):
167+
return self._pipe(lambda chunk, ctx: [x for x in chunk if function(x, ctx) or True])
168+
169+
return self._pipe(lambda chunk, _ctx: [x for x in chunk if function(x) or True]) # type: ignore
131170

132-
return self._pipe(lambda chunk, _ctx: [function(x) or x for x in chunk]) # type: ignore
171+
# Default case for robustness
172+
case _:
173+
raise TypeError(f"tap() argument must be a Transformer or a callable, not {type(arg).__name__}")
133174

134175
def apply[T](self, t: Callable[[Self], "Transformer[In, T]"]) -> "Transformer[In, T]":
135176
"""Apply another pipeline to the current one."""

tests/test_transformer.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,60 @@ def test_tap_with_context(self):
8585
assert result == [1, 2, 3]
8686
assert side_effects == ["item:1", "item:2", "item:3"]
8787

88+
def test_tap_with_transformer(self):
89+
"""Test tap with a transformer for side effects."""
90+
side_effects = []
91+
92+
# Create a side-effect transformer that logs processed values
93+
side_effect_transformer = (
94+
createTransformer(int)
95+
.map(lambda x: x * 10) # Transform for side effect
96+
.tap(lambda x: side_effects.append(x)) # Capture the transformed values
97+
)
98+
99+
# Main transformer that uses the side-effect transformer via tap
100+
main_transformer = (
101+
createTransformer(int)
102+
.map(lambda x: x * 2) # Main transformation
103+
.tap(side_effect_transformer) # Apply side-effect transformer
104+
.map(lambda x: x + 1) # Continue main transformation
105+
)
106+
107+
result = list(main_transformer([1, 2, 3]))
108+
109+
# Main pipeline should produce: [1,2,3] -> [2,4,6] -> [3,5,7]
110+
assert result == [3, 5, 7]
111+
112+
# Side effects should capture: [2,4,6] -> [20,40,60]
113+
assert side_effects == [20, 40, 60]
114+
115+
def test_tap_with_transformer_and_context(self):
116+
"""Test tap with a transformer that uses context."""
117+
side_effects = []
118+
context = PipelineContext({"multiplier": 5, "log_prefix": "processed:"})
119+
120+
# Create a context-aware side-effect transformer
121+
side_effect_transformer = (
122+
createTransformer(int)
123+
.map(lambda x, ctx: x * ctx["multiplier"]) # Use context multiplier
124+
.tap(lambda x, ctx: side_effects.append(f"{ctx['log_prefix']}{x}")) # Log with context prefix
125+
)
126+
127+
# Main transformer
128+
main_transformer = (
129+
createTransformer(int)
130+
.map(lambda x: x + 10) # Main transformation
131+
.tap(side_effect_transformer) # Apply side-effect transformer with context
132+
)
133+
134+
result = list(main_transformer([1, 2, 3], context))
135+
136+
# Main pipeline: [1,2,3] -> [11,12,13]
137+
assert result == [11, 12, 13]
138+
139+
# Side effects: [11,12,13] -> [55,60,65] -> ["processed:55", "processed:60", "processed:65"]
140+
assert side_effects == ["processed:55", "processed:60", "processed:65"]
141+
88142

89143
class TestTransformerChaining:
90144
"""Test chaining multiple transformer operations."""

0 commit comments

Comments
 (0)