Skip to content

Commit e89994b

Browse files
committed
find bound lookup & update tox configuration
With this commit I also include usage of the ruff as linter and formatter
1 parent dcae347 commit e89994b

4 files changed

Lines changed: 167 additions & 129 deletions

File tree

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,9 @@ optional arguments after:
537537

538538
You can also organize pipes using classes as shown below:
539539

540+
1. As a `method` method
541+
542+
```python
540543
>>> class Factor:
541544
... n: int = 10
542545
...
@@ -550,9 +553,12 @@ You can also organize pipes using classes as shown below:
550553
>>> fact = Factor(10)
551554
>>> list([1, 2, 3] | fact.mul)
552555
[10, 20, 30]
556+
>>>
557+
```
553558

554-
Supported on classmethod
559+
1. As a `classmethod`
555560

561+
```python
556562
>>> class Factor:
557563
... n: int = 10
558564
...
@@ -566,22 +572,25 @@ Supported on classmethod
566572
...
567573
>>> list([1, 2, 3] | Factor.mul)
568574
[10, 20, 30]
575+
>>>
576+
```
569577

570578
Supported on classmethod like functions
571579

580+
```py
572581
>>> class Factor:
573-
... n: int = 10
574-
...
575582
... def __init__(self, n: int):
576583
... self.n = n
577584
...
578585
... @Pipe
579-
... def mul(cls, iterable):
580-
... return (x * cls.n for x in iterable)
586+
... @staticmethod
587+
... def mul(iterable):
588+
... return (x * 10 for x in iterable)
581589
...
582590
>>> list([1, 2, 3] | Factor.mul)
583591
[10, 20, 30]
584-
592+
>>>
593+
```
585594

586595
## One-off pipes
587596

pipe.py

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,42 @@
55
__credits__ = """Jérôme Schneider for teaching me the Python datamodel,
66
and all contributors."""
77

8+
import builtins
89
import functools
910
import itertools
1011
import socket
11-
import inspect
1212
import sys
13-
from contextlib import closing
1413
from collections import deque
15-
import builtins
14+
from contextlib import closing
1615

1716

18-
def _is_a_classmethod(func):
17+
class Pipe:
1918
"""
20-
Check if a given function is a class method.
19+
Pipe class enable a sh like infix syntax.
2120
22-
Args:
23-
func (function): The function to check.
21+
This class allows you to create a pipeline of operations by chaining functions
22+
together using the `|` operator. It wraps a function and its arguments, enabling
23+
you to apply the function to an input in a clean and readable manner.
2424
25-
Returns:
26-
bool: True if the function is a class method, False otherwise.
27-
"""
28-
signature = inspect.signature(func)
29-
parameters = list(signature.parameters.values())
30-
return len(parameters) > 0 and parameters[0].name == "cls"
25+
Examples
26+
--------
27+
Create a new Pipe operation:
3128
29+
>>> from pipe import Pipe
30+
>>> @Pipe
31+
... def double(iterable):
32+
... return (x * 2 for x in iterable)
33+
34+
Use the Pipe operation:
35+
36+
>>> result = [1, 2, 3] | double
37+
>>> list(result)
38+
[2, 4, 6]
39+
40+
Notes
41+
-----
42+
...
3243
33-
class Pipe:
34-
"""
35-
Represent a Pipeable Element :
36-
Described as :
37-
first = Pipe(lambda iterable: next(iter(iterable)))
38-
and used as :
39-
print [1, 2, 3] | first
40-
printing 1
41-
42-
Or represent a Pipeable Function :
43-
It's a function returning a Pipe
44-
Described as :
45-
select = Pipe(lambda iterable, pred: (pred(x) for x in iterable))
46-
and used as :
47-
print [1, 2, 3] | select(lambda x: x * 2)
48-
# 2, 4, 6
4944
"""
5045

5146
def __init__(self, function, *args, **kwargs):
@@ -56,8 +51,22 @@ def __init__(self, function, *args, **kwargs):
5651
functools.update_wrapper(self, function)
5752

5853
def __ror__(self, other):
59-
bound_args = [] if self.instance is None else [self.instance]
60-
return self.function(*bound_args, other, *self.args, **self.kwargs)
54+
"""
55+
Implement the reverse pipe operator (`|`) for the object.
56+
57+
Parameters
58+
----------
59+
other : Any
60+
The left-hand operand of the `|` operator.
61+
62+
Returns
63+
-------
64+
Any
65+
The result of applying the stored function to `other` with the
66+
provided arguments and keyword arguments.
67+
68+
"""
69+
return self.function(other, *self.args, **self.kwargs)
6170

6271
def __call__(self, *args, **kwargs):
6372
return Pipe(
@@ -75,27 +84,17 @@ def __repr__(self) -> str:
7584
self.kwargs,
7685
)
7786

78-
def __get__(self, instance, owner):
79-
if instance is None:
80-
if owner is None: # pragma: no cover
81-
return self
82-
if isinstance(self.function, classmethod):
83-
self.instance = owner
84-
self.function = self.function.__func__.__get__(None, owner)
85-
return self
86-
if _is_a_classmethod(self.function):
87-
# function is like a classmethod, but not a classmethod
88-
# only the first argument is the class, name it cls
89-
self.instance = owner
90-
return self # pragma: no cover
91-
return self
92-
self.instance = instance
93-
return self
87+
def __get__(self, instance, owner=None):
88+
return Pipe(
89+
function=self.function.__get__(instance, owner),
90+
*self.args,
91+
*self.kwargs,
92+
)
9493

9594

9695
@Pipe
9796
def take(iterable, qte):
98-
"Yield qte of elements in the given iterable."
97+
"""Yield qte of elements in the given iterable."""
9998
if not qte:
10099
return
101100
for item in iterable:
@@ -107,13 +106,13 @@ def take(iterable, qte):
107106

108107
@Pipe
109108
def tail(iterable, qte):
110-
"Yield qte of elements in the given iterable."
109+
"""Yield qte of elements in the given iterable."""
111110
return deque(iterable, maxlen=qte)
112111

113112

114113
@Pipe
115114
def skip(iterable, qte):
116-
"Skip qte elements in the given iterable, then yield others."
115+
"""Skip qte elements in the given iterable, then yield others."""
117116
for item in iterable:
118117
if qte == 0:
119118
yield item
@@ -257,8 +256,3 @@ def batched(iterable, n):
257256
chain_with = Pipe(itertools.chain)
258257
islice = Pipe(itertools.islice)
259258
izip = Pipe(zip)
260-
261-
if __name__ == "__main__":
262-
import doctest
263-
264-
doctest.testfile("README.md")

pyproject.toml

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,125 @@
11
[build-system]
2-
requires = ["setuptools", "wheel"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
44

55
[project]
66
name = "pipe"
77
description = "Module enabling a sh like infix syntax (using pipes)"
88
readme = "README.md"
9-
license = {text = "MIT License"}
9+
license = { text = "MIT License" }
1010
dynamic = ["version"]
11-
authors = [
12-
{name = "Julien Palard", email = "julien@palard.fr"},
13-
]
11+
authors = [{ name = "Julien Palard", email = "julien@palard.fr" }]
1412
classifiers = [
1513
"Programming Language :: Python",
1614
"Programming Language :: Python :: 3",
17-
"Development Status :: 5 - Production/Stable",
18-
"Intended Audience :: Developers",
15+
"Programming Language :: Python :: 3.8",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
18+
"Programming Language :: Python :: 3.12",
19+
"Programming Language :: Python :: 3.13",
1920
"License :: OSI Approved :: MIT License",
21+
"Intended Audience :: Developers",
22+
"Development Status :: 5 - Production/Stable",
2023
"Operating System :: OS Independent",
2124
"Topic :: Software Development",
2225
"Topic :: Software Development :: Libraries :: Python Modules",
2326
]
2427
requires-python = ">= 3.8"
28+
keywords = ["pipe"]
2529

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

29-
[tool.setuptools]
30-
py-modules = [
31-
"pipe",
33+
[dependency-groups]
34+
ruff = ["ruff>=0.10.0"]
35+
coverage = ["coverage[toml]", "covdefaults"]
36+
dev = [{ include-group = "ruff" }, "pre-commit>=2.21.0", "tox>=4.23.2"]
37+
test = [{ include-group = "coverage" }, "mock", "pytest", "pytest-cov"]
38+
39+
[tool.hatch.metadata]
40+
allow-direct-references = true
41+
42+
[tool.hatch.version]
43+
path = "pipe.py"
44+
45+
[tool.hatch.build.targets.sdist]
46+
include = ["pipe.py"]
47+
48+
[tool.hatch.build.targets.wheel]
49+
include = ["pipe.py"]
50+
51+
[tool.ruff]
52+
lint.pydocstyle.convention = "numpy"
53+
lint.extend-select = ["C", "D", "FURB", "I", "N", "PL", "PTH", "SIM"]
54+
lint.extend-ignore = [
55+
"D100", # Missing docstring in public module
56+
"D101", # Missing docstring in public class
57+
"D102", #
58+
"D103", # Missing docstring in public function
59+
"D105",
3260
]
33-
include-package-data = false
3461

35-
[tool.setuptools.dynamic.version]
36-
attr = "pipe.__version__"
62+
[tool.coverage]
63+
html.show_contexts = true
64+
html.skip_covered = false
65+
run.parallel = true
66+
run.omit = [
67+
"tests/*.py",
68+
# add others
69+
]
70+
71+
72+
[tool.tox]
73+
requires = ["tox>=4.23.2", "tox-uv>=1.13"]
74+
isolated_build = true
75+
env_list = ["ruff", "cov-report", "py38", "py39", "py311", "py312", "py313"]
76+
77+
[tool.tox.env_run_base]
78+
commands = [
79+
[
80+
"coverage",
81+
"run",
82+
"-m",
83+
"pytest",
84+
"--doctest-glob=README.md",
85+
"--junit-xml=.build/tests/pytest_{env_name}_junit.xml",
86+
],
87+
]
88+
set_env.COVERAGE_FILE = "{toxworkdir}/.coverage"
89+
dependency_groups = ["test"]
90+
package = "wheel"
91+
wheel_build_env = ".pkg"
92+
93+
[tool.tox.env.ruff]
94+
recreate = false
95+
skip_install = true
96+
dependency_groups = ["ruff"]
97+
commands = [
98+
# commands
99+
["ruff", "format", "--check", "."],
100+
["ruff", "check", "."],
101+
]
102+
103+
[tool.tox.env.cov-report]
104+
parallel_show_output = true
105+
recreate = false
106+
set_env.COVERAGE_FILE = "{toxworkdir}/.coverage"
107+
skip_install = true
108+
dependency_groups = ["coverage"]
109+
depends = ["py38", "py39", "py311", "py312", "py313"]
110+
commands = [
111+
[
112+
"coverage",
113+
"combine",
114+
],
115+
[
116+
"coverage",
117+
"html",
118+
"-d",
119+
".build/coverage",
120+
],
121+
[
122+
"coverage",
123+
"report",
124+
],
125+
]

0 commit comments

Comments
 (0)