Skip to content

support for chained pipes using Ellipsis #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,46 @@ Don't forget they can be aliased:
>>>
```

## Chained pipes

The Pipe class supports chaining multiple transformations using the `|` operator.

### How It Works

* Use `|` between Pipe instances to create a pipeline, and use the Ellipsis (`...`) operator as the first element instead of an iterable.
* Apply the pipeline to data using `data | pipeline`.

### Examples of chained pipes Usage

```python
@Pipe
def double(iterable):
return (x * 2 for x in iterable)

@Pipe
def square(iterable):
return (x ** 2 for x in iterable)

@Pipe
def increment(iterable):
return (x + 1 for x in iterable)

pipeline = double | square | increment # Chain operations

result = [1, 2, 3] | pipeline # Apply to data
print(list(result)) # Output: [5, 17, 37]
```

Other example:

```py
>>> import pipe
>>> pipeline = ... | pipe.skip(2) | pipe.take(3)
>>> list(range(10) | pipeline)
[2, 3, 4]
>>>
```

## Constructing your own

You can construct your pipes using the `Pipe` class like:
Expand Down Expand Up @@ -533,7 +573,7 @@ optional arguments after:
>>>
```

### Organizing pipes more effectively using classes
## Organizing pipes more effectively using classes

The `@Pipe` decorator isn't just for functions-it also works with classes. You can use it with instance methods, class methods, and static methods to better structure your code while keeping it pipeable.

Expand Down
46 changes: 46 additions & 0 deletions pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def __ror__(self, other):
provided arguments and keyword arguments.

"""
if other is Ellipsis or isinstance(other, Pipe):
return ChainedPipes.chain_with(other, self)
return self.function(other, *self.args, **self.kwargs)

def __call__(self, *args, **kwargs):
Expand All @@ -91,6 +93,50 @@ def __get__(self, instance, owner=None):
)


class ChainedPipes(Pipe):
"""
Chain of pipes for sequential application.

This class enables chaining multiple Pipe objects and applying them
sequentially using the `|` operator. It provides a way to define the
pipes sequence and use it later.

Parameters
----------
*pipes : Pipe
A variable number of Pipe objects to be chained together.

Examples
--------
Define a sequence of pipes and use it:

>>> from pipe import take, skip
>>> pipeline = ... | skip(2) take(3))
>>> list([1, 2, 3, 4, 5, 6] | pipeline)
[3, 4, 5]

"""

def __init__(self, *pipes):
self.pipes = pipes

def __ror__(self, other):
result = other
for pipe in self.pipes:
result = result | pipe
return result

@classmethod
def chain_with(cls, other, ref):
if isinstance(other, ChainedPipes):
return ChainedPipes(*other.pipes, ref)
elif other is Ellipsis:
return ChainedPipes(ref)

def __repr__(self):
return "...\n| %s" % "\n| ".join([str(i) for i in self.pipes])


@Pipe
def take(iterable, qte):
"""Yield qte of elements in the given iterable."""
Expand Down
26 changes: 26 additions & 0 deletions tests/test_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,29 @@ def sample_pipe_with_args(iterable, factor):
real_repr = repr(pipe_instance)
assert "piped::<sample_pipe_with_args>(" in real_repr
assert "3" in real_repr


def test_chained_pipes():
pipeline = ... | pipe.skip(2) | pipe.take(3)

assert list(range(10) | pipeline) == [2, 3, 4]

@pipe.Pipe
def double(iterable):
return (x * 2 for x in iterable)

extended_pipeline = pipeline | double
assert list(range(10) | extended_pipeline) == [4, 6, 8]
assert list(range(10) | pipeline | double) == [4, 6, 8]


def test_chained_pipes_on_bad_constructor():
assert pipe.ChainedPipes.chain_with(None, None) is None


def test_chained_pipes_reps():
pipeline = ... | pipe.skip(2) | pipe.take(3)

repr_pipeline = repr(pipeline)
assert repr(pipe.skip(2)) in repr_pipeline
assert repr(pipe.take(3)) in repr_pipeline