Skip to content

Commit c96b252

Browse files
committed
initial commit
0 parents  commit c96b252

13 files changed

Lines changed: 2326 additions & 0 deletions

File tree

.github/workflows/lint-test.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3+
4+
name: Lint and Test
5+
6+
on:
7+
push:
8+
branches: [ main ]
9+
tags: '**'
10+
pull_request:
11+
branches: [ main ]
12+
# schedule:
13+
# # 6:00 every 7th of month
14+
# - cron: '0 6 7 * *'
15+
16+
jobs:
17+
build:
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
python-version: ["3.12", "3.13"]
23+
os: [ubuntu-latest, windows-latest, macos-latest]
24+
steps:
25+
- uses: actions/checkout@v4
26+
- name: "Setup uv ${{ matrix.python-version }} / ${{ matrix.os }}"
27+
uses: astral-sh/setup-uv@v5
28+
with:
29+
python-version: ${{ matrix.python-version }}
30+
- name: Build
31+
run: uv sync --dev
32+
- name: Lint
33+
run: |
34+
uv run ruff check
35+
uv run mypy .
36+
- name: Test
37+
run: |
38+
uv run pytest

.github/workflows/package.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Package
2+
# TODO... publish to PyPI
3+
4+
on:
5+
push:
6+
branches: [ main ]
7+
tags: '**'
8+
workflow_run:
9+
workflows: [ "Lint and Test" ]
10+
types:
11+
- completed
12+
13+
jobs:
14+
package:
15+
runs-on: ubuntu-latest
16+
# environment: pypi
17+
# permissions:
18+
# id-token: write
19+
steps:
20+
- uses: actions/checkout@v4
21+
- name: Install prerequisites
22+
uses: astral-sh/setup-uv@v5
23+
- name: Install project
24+
run: uv sync --all-groups
25+
- name: Build wheel
26+
run: uv build --wheel
27+
# - name: Publish package
28+
# run: uv publish
29+
- name: Upload built package as artifact
30+
uses: actions/upload-artifact@v4
31+
with:
32+
path: dist/**
33+

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dist/
2+
*.egg-info/
3+
__pycache__/
4+
.eggs/
5+
6+
.venv*/
7+
.vscode/
8+
9+
coverage.info
10+
11+
.env
12+
uv.lock
13+
.python-version
14+
15+
.coverage
16+
htmlcov/

LICENCE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# MIT Licence
2+
3+
Copyright © 2025 Andrew Smith
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicence, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.**

README.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# itr - a powerful `Iterable` wrapper
2+
3+
Heavily inspired by rust's [Iterator trait](https://doc.rust-lang.org/std/iter/trait.Iterator.html),
4+
with added pythonicness (such as `starmap`)
5+
6+
7+
### Functionality
8+
9+
`Itr` wraps python Iterators, Iterables and Generators providing the ability to arbitrarily chain (and lazily evaluate) methods, e.g.
10+
11+
```py
12+
>>> from itr import Itr
13+
>>> Itr(range(100)).rev().step_by(4).skip(10).map(lambda x: x * x).filter(lambda x: x % 3 == 0).for_each(print)
14+
2304
15+
1296
16+
576
17+
144
18+
0
19+
>>>
20+
```
21+
22+
For reference, the equivalent expression using builtins and `itertools`:
23+
24+
```py
25+
from iterools import islice
26+
27+
for item in filter(
28+
lambda x: x % 3 == 0,
29+
map(lambda x: x * x, islice(islice(reversed(range(100)), None, None, 4), 10, None)),
30+
):
31+
print(item)
32+
```
33+
34+
35+
In many cases the `Itr` simply provides a wrapper around `functools` enabling chaining of operations in a left-to-right syntax.
36+
37+
Whilst many methods return either `self` or another instance/instances of `Itr` (thus allowing for chaining and lazy evaluation), some methods consume and return actual values (such as `collect`, `next`, `next_chunk`) or aggregates (such as `count`,`reduce`,`max`) from the iterable (i.e. eager evaluation). Note that:
38+
39+
- iterators can only be consumed once (use `copy`, `cycle` or `repeat` as necessary)
40+
- it's not possible to rewind, but the next value in the sequence can be "previewed" using the `peek` method. Doing this a lot is unlikely to be efficent though, as it copies the iterator.
41+
- be wary of open-ended iterators, eager evaluation can lead to infinite loops.
42+
43+
Methods implemented (see the docstrings for more details):
44+
45+
`__iter__`, `__next__`, `all`, `any`, `batched`, `chain`, `collect`, `copy`, `count`, `cycle`, `diff`, `enumerate`, `filter`, `find`, `flat_map`, `flatten`, `fold`, `for_each`, `groupby`, `interleave`, `intersperse`, `last`, `map`, `max`, `min`, `next`, `next_chunk`, `nth`, `pairwise`, `partition`, `peek`, `product`, `reduce`, `repeat`, `rev`, `rolling`, `skip`, `skip_while`, `starmap`, `step_by`, `take`, `take_while`, `unzip`, `zip`
46+
47+
This list is auto-generated by `Itr` introspecting itself, which also generates [more detailed documentation](autodoc.md)
48+
49+
The code is here: [introspect.py](introspect.py)
50+
51+
## Examples
52+
53+
### Inputs
54+
55+
Accepts iterables (sequences and ranges), iterators or generators:
56+
57+
```py
58+
from itr import Itr
59+
60+
it1 = Itr([0, 1, 2, 3, 4])
61+
assert it1.collect() == (0, 1, 2, 3, 4)
62+
63+
it2 = Itr(range(5))
64+
assert it2.collect() == (0, 1, 2, 3, 4)
65+
66+
class Fib(Iterator):
67+
def __init__(self) -> None:
68+
self.a = 0
69+
self.b = 1
70+
71+
def __iter__(self) -> Self:
72+
return self
73+
74+
def __next__(self) -> int:
75+
ret = self.a
76+
self.a, self.b = self.b, self.a + self.b
77+
return ret
78+
79+
it3 = Itr(Fib())
80+
assert it3.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
81+
82+
def fibgen() -> Generator[int, None, None]:
83+
a, b = 0, 1
84+
while True:
85+
yield a
86+
a, b = b, a + b
87+
88+
it4 = Itr(fibgen())
89+
assert it4.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
90+
```
91+
92+
### Outputs
93+
94+
By default, the `collect()` method returns a `tuple`, but this can be changed to `list`, `set` or `dict`. Here's how to group a words by their length into a dictionary:
95+
96+
```py
97+
>>> from itr import Itr
98+
>>> Itr(("apple", "banana", "carrot")).groupby(len).collect(dict)
99+
{5: ('apple',), 6: ('banana', 'carrot')}
100+
>>>
101+
```
102+
103+
```py
104+
>>> {k: tuple(v) for k, v in itertools.groupby(("apple", "banana", "carrot"), key=len)}
105+
```
106+
107+
108+
NB using `dict` requires an iterable that produces 2-tuples.
109+
110+
### Composition
111+
112+
Numerical differentiation and integration:
113+
114+
```py
115+
from itr import Itr
116+
import numpy as np
117+
118+
x = Itr(np.linspace(0, 1, 11))
119+
# need a copy as the original x is consumed in the zip method
120+
xy = x.copy().zip(x.map(lambda xi: xi * xi / 2))
121+
122+
def diff1central(xy):
123+
(x0, y0), (x1, _y1), (x2, y2) = xy
124+
return x1, (y2 - y0) / (x2 - x0)
125+
126+
print(
127+
xy.copy() # need a copy as reusing xy below
128+
.rolling(3)
129+
.map(diff1central) # x, dy/dx pairs
130+
.collect()
131+
)
132+
133+
# similarly, the definite integral x**3/6 -> using the trapezoidal rule over 0,1 = 1/6
134+
def trapz(sum, xy):
135+
(x0, y0), (x1, y1) = xy
136+
return sum + 0.5 * (y0 + y1) * (x1 - x0)
137+
138+
print(xy.pairwise().fold(0, trapz))
139+
140+
# ...or an alternative implementation using starmap and two levels of flattening
141+
142+
def trapz2(x0, y0, x1, y1): # type: ignore[no-untyped-def] # noqa: D103
143+
return (y0 + y1) * (x1 - x0) / 2
144+
145+
print(xy.copy().pairwise().flatten().flatten().batched(4).starmap(trapz2).reduce(add))
146+
147+
```

0 commit comments

Comments
 (0)