Releasing pipe v2.0

Posted on Mon 23 May 2022 in blog

Introducing partially parametrized pipes!

pipe is a very old project of mine, proudly showing 1.3k stars on github!

I don't maintain it a lot, but today I'm announcing a new release introducing a nice feature: partially parametrized pipes.

If you're familiar with currying or functools.partial you won't get lost.

Show me!

Before pipe 2, the following were already valid:

>>> from random import randint
>>> from pipe import where, sort
>>>
>>> negative = where(lambda x: x < 0)
>>> positive = where(lambda x: x > 0)
>>>
>>> numbers = [randint(-10, 10) for _ in range(10)]
>>>
>>> numbers | positive | sort
[7, 10]
>>> numbers | negative | sort
[-10, -9, -8, -7, -4, -4, -2]

It allows to name things, naming things make code more readable so I wanted more of it.

It was probably underrated, I like to do this kind of things:

>>> isort = sort(key=lambda x: x**2)
>>> numbers | isort
[0, -2, -4, -4, -7, 7, -8, -9, 10, -10]

But it failed as soon as you try go deeper:

>>> numbers | isort(reverse=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pipe.py", line 100, in __ror__
    return self.function(other)
  File "pipe.py", line 103, in <lambda>
    return Pipe(lambda x: self.function(x, *args, **kwargs))
TypeError: Pipe.__call__.<locals>.<lambda>() got an unexpected keyword argument 'reverse'

Call it a bug, that's what's pipe 2 fixes:

>>> from pipe import where, sort
>>> from random import randint
>>> numbers = [randint(-10, 10) for _ in range(10)]
>>>
>>> isort = sort(key=lambda x: x**2)
>>> numbers | isort(reverse=True)
[-10, -9, 8, -6, 6, -4, 4, 4, 2, 1]
>>>

Or in the other way around:

>>> rsort = sort(reverse=True)
>>> numbers | rsort
[8, 6, 4, 4, 2, 1, -4, -6, -9, -10]
>>> numbers | rsort(key=lambda x: x**2)
[-10, -9, 8, -6, 6, -4, 4, 4, 2, 1]

From here you can build many specialized blocks from a single pipe, here's a better example:

import re

from pipe import Pipe


@Pipe
def grep(iterable, pattern, flags=0, invert=False):
    """Mimics grep."""
    for line in iterable:
        match = re.match(pattern, line, flags=flags)
        if (match and not invert) or (not match and invert):
            yield line

Build the small reusable blocks:

igrep = grep(flags=re.I)
vgrep = grep(invert=True)
vigrep = igrep(invert=True)
https = igrep("https://")
not_https = https(invert=True)
...

And they all do what you think they do:

>>> lines = ["https://python.org", "http://detectportal.firefox.com/", "Just no an URL", "HTTPS://afpy.org"]
>>> for url in lines | https:
...     print(url)
...
https://python.org
HTTPS://afpy.org
>>> for url in lines | not_https:
...     print(url)
...
http://detectportal.firefox.com/
Just no an URL

Remember I said currying? Yes you can abuse the syntax, but please don't:

>>> from pipe import sort
>>> lines | grep()(invert=True)()(flags=re.I)()("https://")() | sort
['Just not an URL', 'http://detectportal.firefox.com/']

As I like to say: « It's not because it's possible that you should do it ».