Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,64 @@ optional arguments after:
>>>
```

### Organizing using class

You can also organize pipes using classes as shown below:

1. As a `method` method

```python
>>> class Factor:
... n: int = 10
Comment thread
AdrianCert marked this conversation as resolved.
Outdated
...
... def __init__(self, n: int):
... self.n = n
...
... @Pipe
... def mul(self, iterable):
... return (x * self.n for x in iterable)
...
>>> fact = Factor(10)
>>> list([1, 2, 3] | fact.mul)
[10, 20, 30]
>>>
```

1. As a `classmethod`

```python
>>> class Factor:
... n: int = 10
...
... def __init__(self, n: int):
... self.n = n
Comment thread
AdrianCert marked this conversation as resolved.
Outdated
...
... @Pipe
... @classmethod
... def mul(cls, iterable):
... return (x * cls.n for x in iterable)
...
>>> list([1, 2, 3] | Factor.mul)
[10, 20, 30]
>>>
```

Supported on classmethod like functions
Comment thread
AdrianCert marked this conversation as resolved.
Outdated

```py
>>> class Factor:
... def __init__(self, n: int):
... self.n = n
...
Comment thread
AdrianCert marked this conversation as resolved.
Outdated
... @Pipe
... @staticmethod
... def mul(iterable):
... return (x * 10 for x in iterable)
...
>>> list([1, 2, 3] | Factor.mul)
[10, 20, 30]
>>>
```

## One-off pipes

Expand Down
100 changes: 69 additions & 31 deletions pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,96 @@
__credits__ = """Jérôme Schneider for teaching me the Python datamodel,
and all contributors."""

import builtins
import functools
import itertools
import socket
import sys
from contextlib import closing
from collections import deque
import builtins
from contextlib import closing


class Pipe:
"""
Represent a Pipeable Element :
Described as :
first = Pipe(lambda iterable: next(iter(iterable)))
and used as :
print [1, 2, 3] | first
printing 1

Or represent a Pipeable Function :
It's a function returning a Pipe
Described as :
select = Pipe(lambda iterable, pred: (pred(x) for x in iterable))
and used as :
print [1, 2, 3] | select(lambda x: x * 2)
# 2, 4, 6
Pipe class enable a sh like infix syntax.

This class allows you to create a pipeline of operations by chaining functions
together using the `|` operator. It wraps a function and its arguments, enabling
you to apply the function to an input in a clean and readable manner.

Examples
--------
Create a new Pipe operation:

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

Use the Pipe operation:

>>> result = [1, 2, 3] | double
>>> list(result)
[2, 4, 6]

Notes
-----
...

"""

def __init__(self, function, *args, **kwargs):
Comment thread
JulienPalard marked this conversation as resolved.
self.function = lambda iterable, *args2, **kwargs2: function(
iterable, *args, *args2, **kwargs, **kwargs2
)
self.args = args
self.kwargs = kwargs
self.function = function
self.instance = None
Comment thread
AdrianCert marked this conversation as resolved.
Outdated
functools.update_wrapper(self, function)

def __ror__(self, other):
return self.function(other)
"""
Implement the reverse pipe operator (`|`) for the object.

Parameters
----------
other : Any
The left-hand operand of the `|` operator.

Returns
-------
Any
The result of applying the stored function to `other` with the
provided arguments and keyword arguments.

"""
return self.function(other, *self.args, **self.kwargs)

def __call__(self, *args, **kwargs):
return Pipe(
lambda iterable, *args2, **kwargs2: self.function(
iterable, *args, *args2, **kwargs, **kwargs2
)
self.function,
*self.args,
*args,
**self.kwargs,
**kwargs,
)

def __repr__(self) -> str:
return "piped::<%s>(*%s, **%s)" % (
self.function.__name__,
self.args,
self.kwargs,
)

def __get__(self, instance, owner=None):
return Pipe(
function=self.function.__get__(instance, owner),
*self.args,
*self.kwargs,
)


@Pipe
def take(iterable, qte):
"Yield qte of elements in the given iterable."
"""Yield qte of elements in the given iterable."""
if not qte:
return
for item in iterable:
Expand All @@ -63,13 +106,13 @@ def take(iterable, qte):

@Pipe
def tail(iterable, qte):
"Yield qte of elements in the given iterable."
"""Yield qte of elements in the given iterable."""
return deque(iterable, maxlen=qte)


@Pipe
def skip(iterable, qte):
"Skip qte elements in the given iterable, then yield others."
"""Skip qte elements in the given iterable, then yield others."""
for item in iterable:
if qte == 0:
yield item
Expand Down Expand Up @@ -213,8 +256,3 @@ def batched(iterable, n):
chain_with = Pipe(itertools.chain)
islice = Pipe(itertools.islice)
izip = Pipe(zip)

if __name__ == "__main__":
import doctest

doctest.testfile("README.md")
117 changes: 103 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,125 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "pipe"
description = "Module enabling a sh like infix syntax (using pipes)"
readme = "README.md"
license = {text = "MIT License"}
license = { text = "MIT License" }
dynamic = ["version"]
authors = [
{name = "Julien Palard", email = "julien@palard.fr"},
]
authors = [{ name = "Julien Palard", email = "julien@palard.fr" }]
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">= 3.8"
keywords = ["pipe"]

[project.urls]
repository = "https://github.com/JulienPalard/Pipe"

[tool.setuptools]
py-modules = [
"pipe",
[dependency-groups]
ruff = ["ruff>=0.10.0"]
coverage = ["coverage[toml]", "covdefaults"]
Comment thread
AdrianCert marked this conversation as resolved.
dev = [{ include-group = "ruff" }, "pre-commit>=2.21.0", "tox>=4.23.2"]
test = [{ include-group = "coverage" }, "mock", "pytest", "pytest-cov"]

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.version]
path = "pipe.py"

[tool.hatch.build.targets.sdist]
include = ["pipe.py"]

[tool.hatch.build.targets.wheel]
include = ["pipe.py"]

[tool.ruff]
lint.pydocstyle.convention = "numpy"
lint.extend-select = ["C", "D", "FURB", "I", "N", "PL", "PTH", "SIM"]
lint.extend-ignore = [
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", #
"D103", # Missing docstring in public function
"D105",
]
include-package-data = false

[tool.setuptools.dynamic.version]
attr = "pipe.__version__"
[tool.coverage]
html.show_contexts = true
html.skip_covered = false
run.parallel = true
run.omit = [
"tests/*.py",
# add others
]


[tool.tox]
requires = ["tox>=4.23.2", "tox-uv>=1.13"]
isolated_build = true
env_list = ["ruff", "cov-report", "py38", "py39", "py311", "py312", "py313"]

[tool.tox.env_run_base]
commands = [
[
"coverage",
"run",
"-m",
"pytest",
"--doctest-glob=README.md",
"--junit-xml=.build/tests/pytest_{env_name}_junit.xml",
],
]
set_env.COVERAGE_FILE = "{toxworkdir}/.coverage"
dependency_groups = ["test"]
package = "wheel"
wheel_build_env = ".pkg"
Comment thread
JulienPalard marked this conversation as resolved.

[tool.tox.env.ruff]
recreate = false
skip_install = true
dependency_groups = ["ruff"]
commands = [
# commands
["ruff", "format", "--check", "."],
["ruff", "check", "."],
]

[tool.tox.env.cov-report]
parallel_show_output = true
recreate = false
set_env.COVERAGE_FILE = "{toxworkdir}/.coverage"
skip_install = true
dependency_groups = ["coverage"]
depends = ["py38", "py39", "py311", "py312", "py313"]
commands = [
[
"coverage",
"combine",
],
[
"coverage",
"html",
"-d",
".build/coverage",
],
[
"coverage",
"report",
],
]
28 changes: 28 additions & 0 deletions tests/test_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,31 @@ def test_enumerate():
data = [4, "abc", {"key": "value"}]
expected = [(5, 4), (6, "abc"), (7, {"key": "value"})]
assert list(data | pipe.enumerate(start=5)) == expected


def test_class_support():
class Factory:
n = 10

@pipe.Pipe
def mul(self, iterable):
return (x * self.n for x in iterable)

assert list([1, 2, 3] | Factory().mul) == [10, 20, 30]


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

assert repr(sample_pipe) == "piped::<sample_pipe>(*(), **{})"

@pipe.Pipe
def sample_pipe_with_args(iterable, factor):
return (x * factor for x in iterable)

pipe_instance = sample_pipe_with_args(3)
real_repr = repr(pipe_instance)
assert "piped::<sample_pipe_with_args>(" in real_repr
assert "3" in real_repr
Loading