pythonic-fp.iterables 3.0.0__py3-none-any.whl → 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ # Copyright 2023-2024 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Library of functions for iterating iterables.
16
+
17
+ +-----------+-------------------------------------------+
18
+ | Module | Description |
19
+ +===========+===========================================+
20
+ | merging | Concatenating and merging iterables |
21
+ +-----------+-------------------------------------------+
22
+ | drop_take | Dropping and taking values from iterables |
23
+ +-----------+-------------------------------------------+
24
+ | folding | Reducing and accumulating iterables |
25
+ +-----------+-------------------------------------------+
26
+
27
+ Assumptions:
28
+
29
+ - iterables are not necessarily iterators
30
+ - at all times iterator protocol is assumed to be followed
31
+
32
+ - all iterators are assumed to be iterable
33
+ - for all iterators ``foobar`` we assume ``iter(foobar) is foobar``
34
+
35
+ """
36
+
37
+ __author__ = 'Geoffrey R. Scheller'
38
+ __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
39
+ __license__ = 'Apache License 2.0'
@@ -0,0 +1,177 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Module pythonic_fp.iterables.drop_take"""
16
+
17
+ from __future__ import annotations
18
+
19
+ __author__ = 'Geoffrey R. Scheller'
20
+ __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
21
+ __license__ = 'Apache License 2.0'
22
+
23
+ from collections.abc import Callable, Iterable, Iterator
24
+ from pythonic_fp.gadgets.box import Box
25
+ from .merging import concat
26
+
27
+ __all__ = [
28
+ 'drop',
29
+ 'drop_while',
30
+ 'take',
31
+ 'take_while',
32
+ 'take_split',
33
+ 'take_while_split',
34
+ ]
35
+
36
+
37
+ ## dropping and taking
38
+
39
+
40
+ def drop[D](iterable: Iterable[D], n: int) -> Iterator[D]:
41
+ """Drop the next n values from iterable.
42
+
43
+ :param iterable: iterable whose values are to be dropped
44
+ :param n: number of values to be dropped
45
+ :return: an iterator of the remaining values
46
+
47
+ """
48
+ iterator = iter(iterable)
49
+ for _ in range(n):
50
+ try:
51
+ next(iterator)
52
+ except StopIteration:
53
+ break
54
+ return iterator
55
+
56
+
57
+ def drop_while[D](
58
+ iterable: Iterable[D],
59
+ pred: Callable[[D], bool]
60
+ ) -> Iterator[D]:
61
+ """Drop initial values from iterable while predicate is true.
62
+
63
+ :param iterable: iterable whose values are to be dropped
64
+ :param pred: Boolean valued function, the "predicate"
65
+ :return: an iterator beginning where pred returned false
66
+
67
+ """
68
+ iterator = iter(iterable)
69
+ while True:
70
+ try:
71
+ value = next(iterator)
72
+ if not pred(value):
73
+ iterator = concat((value,), iterator)
74
+ break
75
+ except StopIteration:
76
+ break
77
+ return iterator
78
+
79
+
80
+ def take[D](iterable: Iterable[D], n: int) -> Iterator[D]:
81
+ """Return an iterator of up to n initial values of an iterable.
82
+
83
+ :param iterable: iterable providing the values to be taken
84
+ :param n: number of values to be dropped
85
+ :return: an iterator of up to n initial values from iterable
86
+
87
+ """
88
+ iterator = iter(iterable)
89
+ for _ in range(n):
90
+ try:
91
+ value = next(iterator)
92
+ yield value
93
+ except StopIteration:
94
+ break
95
+
96
+
97
+ def take_while[D](
98
+ iterable: Iterable[D],
99
+ pred: Callable[[D], bool]
100
+ ) -> Iterator[D]:
101
+ """Yield values from iterable while predicate is true.
102
+
103
+ .. warning::
104
+ Risk of value loss if iterable is multiple referenced iterator.
105
+
106
+ :param iterable: iterable providing the values to be taken
107
+ :param pred: Boolean valued function, the "predicate"
108
+ :return: an Iterator of of values from iterable until predicate false
109
+
110
+ """
111
+ iterator = iter(iterable)
112
+ while True:
113
+ try:
114
+ value = next(iterator)
115
+ if pred(value):
116
+ yield value
117
+ else:
118
+ break
119
+ except StopIteration:
120
+ break
121
+
122
+
123
+ def take_split[D](
124
+ iterable: Iterable[D],
125
+ n: int
126
+ ) -> tuple[Iterator[D], Iterator[D]]:
127
+ """Same as take except also return iterator of remaining values.
128
+
129
+ .. Warning::
130
+
131
+ **CONTRACT:** Do not access the second returned iterator until the
132
+ first one is exhausted.
133
+
134
+ :param iterable: iterable providing the values to be taken
135
+ :param n: maximum number of values to be taken
136
+ :return: an iterator of values taken and an iterator of remaining values
137
+
138
+ """
139
+ iterator = iter(iterable)
140
+ itn = take(iterator, n)
141
+
142
+ return itn, iterator
143
+
144
+
145
+ def take_while_split[D](
146
+ iterable: Iterable[D], pred: Callable[[D], bool]
147
+ ) -> tuple[Iterator[D], Iterator[D]]:
148
+ """Yield values from iterable while predicate is true.
149
+
150
+ .. Warning::
151
+
152
+ **CONTRACT:** Do not access the second returned iterator until
153
+ the first one is exhausted.
154
+
155
+ :param iterable: iterable providing the values to be taken
156
+ :param pred: Boolean valued function
157
+ :return: tuple containing an iterator of values taken and an iterator of remaining values
158
+
159
+ """
160
+ def _take_while(
161
+ iterator: Iterator[D], pred: Callable[[D], bool], val: Box[D]
162
+ ) -> Iterator[D]:
163
+ while True:
164
+ try:
165
+ val.put(next(iterator))
166
+ if pred(val.get()):
167
+ yield val.pop()
168
+ else:
169
+ break
170
+ except StopIteration:
171
+ break
172
+
173
+ iterator = iter(iterable)
174
+ value: Box[D] = Box()
175
+ it_pred = _take_while(iterator, pred, value)
176
+
177
+ return (it_pred, concat(value, iterator))
@@ -0,0 +1,294 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Module pythonic_fp.iterables.folding"""
16
+
17
+ from __future__ import annotations
18
+
19
+ __author__ = 'Geoffrey R. Scheller'
20
+ __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
21
+ __license__ = 'Apache License 2.0'
22
+
23
+ from collections.abc import Callable, Iterable, Iterator
24
+ from typing import cast, Never
25
+ from pythonic_fp.fptools.function import negate, swap
26
+ from pythonic_fp.fptools.maybe import MayBe
27
+ from pythonic_fp.singletons.novalue import NoValue
28
+ from .drop_take import drop_while, take_while_split
29
+
30
+ __all__ = [
31
+ 'accumulate',
32
+ 'reduce_left',
33
+ 'fold_left',
34
+ 'maybe_fold_left',
35
+ 'sc_reduce_left',
36
+ 'sc_reduce_right',
37
+ ]
38
+
39
+ def accumulate[D, L](
40
+ iterable: Iterable[D],
41
+ f: Callable[[L, D], L],
42
+ initial: L | NoValue = NoValue()
43
+ ) -> Iterator[L]:
44
+ """Returns an iterator of partial fold values.
45
+
46
+ A pure Python version of standard library's ``itertools.accumulate``
47
+
48
+ - function ``f`` does not default to addition (for typing flexibility)
49
+ - begins accumulation with an "optional" ``initial`` value
50
+
51
+ :param iterable: iterable to be folded
52
+ :param f: two parameter function, first parameter for accumulated value
53
+ :param initial: "optional" initial value to start fold
54
+ :return: an iterator of the intermediate fold values
55
+
56
+ """
57
+ it = iter(iterable)
58
+ try:
59
+ it0 = next(it)
60
+ except StopIteration:
61
+ if initial is NoValue():
62
+ return
63
+ yield cast(L, initial)
64
+ else:
65
+ if initial is not NoValue():
66
+ init = cast(L, initial)
67
+ yield init
68
+ acc = f(init, it0)
69
+ for ii in it:
70
+ yield acc
71
+ acc = f(acc, ii)
72
+ yield acc
73
+ else:
74
+ acc = cast(L, it0) # in this case L = D
75
+ for ii in it:
76
+ yield acc
77
+ acc = f(acc, ii)
78
+ yield acc
79
+
80
+
81
+ def reduce_left[D](
82
+ iterable: Iterable[D],
83
+ f: Callable[[D, D], D]
84
+ ) -> D | Never:
85
+ """Fold an iterable left with a function.
86
+
87
+ .. Warning::
88
+
89
+ This function never return if given an infinite iterable.
90
+
91
+ .. Warning::
92
+
93
+ This function does not catch or re-raises exceptions from ``f``.
94
+
95
+ :param iterable: iterable to be reduced (folded)
96
+ :param f: two parameter function, first parameter for accumulated value
97
+ :return: reduced value from the iterable
98
+ :raises StopIteration: when called on an empty iterable
99
+ :raises Exception: does not catch any exceptions from ``f``
100
+
101
+ """
102
+ it = iter(iterable)
103
+ try:
104
+ acc = next(it)
105
+ except StopIteration as exc:
106
+ msg = 'Attempt to reduce an empty iterable?'
107
+ raise StopIteration(msg) from exc
108
+
109
+ for v in it:
110
+ acc = f(acc, v)
111
+
112
+ return acc
113
+
114
+
115
+ def fold_left[D, L](
116
+ iterable: Iterable[D],
117
+ f: Callable[[L, D], L],
118
+ initial: L
119
+ ) -> L | Never:
120
+ """Fold an iterable left with a function and initial value.
121
+
122
+ - not restricted to ``__add__`` for the folding function
123
+ - initial value required, does not default to ``0`` for initial value
124
+ - handles non-numeric data just find
125
+
126
+ .. Warning::
127
+
128
+ This function never return if given an infinite iterable.
129
+
130
+ .. Warning::
131
+
132
+ This function does not catch any exceptions ``f`` may raise.
133
+
134
+ :param iterable: iterable to be folded
135
+ :param f: two parameter function, first parameter for accumulated value
136
+ :param initial: mandatory initial value to start fold
137
+ :return: the folded value
138
+
139
+ """
140
+ acc = initial
141
+ for v in iterable:
142
+ acc = f(acc, v)
143
+ return acc
144
+
145
+
146
+ def maybe_fold_left[D, L](
147
+ iterable: Iterable[D],
148
+ f: Callable[[L, D], L],
149
+ initial: L | NoValue = NoValue()
150
+ ) -> MayBe[L] | Never:
151
+ """Folds an iterable left with an "optional" initial value..
152
+
153
+ - when an initial value is not given then ``L = D``
154
+ - if iterable empty and no ``initial`` value given, return ``MayBe()``
155
+
156
+ .. Warning::
157
+
158
+ This function never return if given an infinite iterable.
159
+
160
+ .. Warning::
161
+
162
+ This function returns a ``MayBe()`` when ``f`` raises any
163
+ exception what-so-ever. The exception is thrown away.
164
+
165
+ :param iterable: The iterable to be folded.
166
+ :param f: First argument is for the accumulated value.
167
+ :param initial: Mandatory initial value to start fold.
168
+ :return: MayBe of a successfully folded value, otherwise MayBe()
169
+
170
+ """
171
+ acc: L
172
+ it = iter(iterable)
173
+ if initial is NoValue():
174
+ try:
175
+ acc = cast(L, next(it)) # in this case L = D
176
+ except StopIteration:
177
+ return MayBe()
178
+ else:
179
+ acc = cast(L, initial)
180
+
181
+ for v in it:
182
+ try:
183
+ acc = f(acc, v)
184
+ except Exception:
185
+ return MayBe()
186
+
187
+ return MayBe(acc)
188
+
189
+
190
+ def sc_reduce_left[D](
191
+ iterable: Iterable[D],
192
+ f: Callable[[D, D], D],
193
+ start: Callable[[D], bool] = (lambda d: True),
194
+ stop: Callable[[D], bool] = (lambda d: False),
195
+ include_start: bool = True,
196
+ include_stop: bool = True,
197
+ ) -> tuple[MayBe[D], Iterator[D]]:
198
+ """Short circuit version of a left reduce.
199
+
200
+ Useful for infinite iterables.
201
+
202
+ Behavior for default arguments will
203
+
204
+ - left reduce finite iterable
205
+ - start folding immediately
206
+ - continue folding until end (of a possibly infinite iterable)
207
+
208
+ :param iterable: iterable to be reduced from the left
209
+ :param f: two parameter function, first parameter for accumulated value
210
+ :param start: delay starting the fold until it returns true
211
+ :param stop: prematurely stop the fold when it returns true
212
+ :param include_start: if true, include fold starting value in fold
213
+ :param include_stop: if true, include stopping value in fold
214
+ :return: tuple of a MayBe of the folded value and iterator of remaining iterables
215
+
216
+ """
217
+ it_start = drop_while(iterable, negate(start))
218
+ if not include_start:
219
+ try:
220
+ next(it_start)
221
+ except StopIteration:
222
+ pass
223
+ it_reduce, it_rest = take_while_split(it_start, negate(stop))
224
+ mb_reduced = maybe_fold_left(it_reduce, f)
225
+ if include_stop:
226
+ if mb_reduced:
227
+ try:
228
+ last = next(it_rest)
229
+ mb_reduced = MayBe(f(mb_reduced.get(), last))
230
+ except StopIteration:
231
+ pass
232
+ else:
233
+ try:
234
+ last = next(it_rest)
235
+ mb_reduced = MayBe(last)
236
+ except StopIteration:
237
+ pass
238
+
239
+ return (mb_reduced, it_rest)
240
+
241
+
242
+ def sc_reduce_right[D](
243
+ iterable: Iterable[D],
244
+ f: Callable[[D, D], D],
245
+ start: Callable[[D], bool] = (lambda d: False),
246
+ stop: Callable[[D], bool] = (lambda d: False),
247
+ include_start: bool = True,
248
+ include_stop: bool = True,
249
+ ) -> tuple[MayBe[D], Iterator[D]]:
250
+ """Short circuit version of a right reduce.
251
+
252
+ Useful for infinite and non-reversible iterables.
253
+
254
+ Behavior for default arguments will
255
+
256
+ - right reduce finite iterable
257
+ - start folding at end (of a possibly infinite iterable)
258
+ - continue reducing right until beginning
259
+
260
+ :param iterable: iterable to be reduced from the right
261
+ :param f: two parameter function, second parameter for accumulated value
262
+ :param start: delay starting the fold until it returns true
263
+ :param stop: prematurely stop the fold when it returns true
264
+ :param include_start: if true, include fold starting value in fold
265
+ :param include_stop: if true, include stopping value in fold
266
+ :return: tuple of a MayBe of the folded value and iterator of remaining iterables
267
+
268
+ """
269
+ it_start, it_rest = take_while_split(iterable, negate(start))
270
+ list1 = list(it_start)
271
+ if include_start:
272
+ try:
273
+ begin = next(it_rest)
274
+ except StopIteration:
275
+ pass
276
+ else:
277
+ list1.append(begin)
278
+
279
+ list1.reverse()
280
+ it_reduce, it_stop = take_while_split(list1, negate(stop))
281
+
282
+ mb_reduced = maybe_fold_left(it_reduce, swap(f))
283
+ if include_stop:
284
+ try:
285
+ end = next(it_stop)
286
+ except StopIteration:
287
+ pass
288
+ else:
289
+ if mb_reduced:
290
+ mb_reduced = MayBe(f(end, mb_reduced.get()))
291
+ else:
292
+ mb_reduced = MayBe(end)
293
+
294
+ return (mb_reduced, it_rest)
@@ -0,0 +1,148 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Module pythonic_fp.iterables.merging"""
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Iterable, Iterator
20
+ from enum import auto, Enum
21
+
22
+ __all__ = [
23
+ 'MergeEnum',
24
+ 'concat',
25
+ 'merge',
26
+ 'exhaust',
27
+ 'blend',
28
+ ]
29
+
30
+ class MergeEnum(Enum):
31
+ """Iterable Blending Enums.
32
+
33
+ - **MergeEnum.Concat:** Concatenate first to last
34
+ - **MergeEnum.Merge:** Merge until one is exhausted
35
+ - **MergeEnum.Exhaust:** Merge until all are exhausted
36
+
37
+ """
38
+ Concat = auto()
39
+ Merge = auto()
40
+ Exhaust = auto()
41
+
42
+
43
+ def concat[D](*iterables: Iterable[D]) -> Iterator[D]:
44
+ """Sequentially concatenate multiple iterables together.
45
+
46
+ .. warning::
47
+ An infinite iterable will prevent subsequent iterables from
48
+ yielding any values.
49
+
50
+ .. note::
51
+
52
+ Performant to the standard library's ``itertools.chain``.
53
+
54
+ :param iterables: iterables to concatenate
55
+ :return: iterator of concatenated values from the iterables
56
+
57
+ """
58
+ for iterator in map(lambda x: iter(x), iterables):
59
+ while True:
60
+ try:
61
+ value = next(iterator)
62
+ yield value
63
+ except StopIteration:
64
+ break
65
+
66
+
67
+ def merge[D](
68
+ *iterables: Iterable[D],
69
+ yield_partials: bool = False
70
+ ) -> Iterator[D]:
71
+ """Merge together ``iterables`` until one is exhausted.
72
+
73
+ .. note::
74
+
75
+ When ``yield_partials`` is true, then any unmatched values from other iterables
76
+ already yielded when the first iterable is exhausted are yielded. This prevents
77
+ data lose if any of the iterables are iterators with external references.
78
+
79
+ :param iterables: iterables to merge until one gets exhausted
80
+ :param yield_partials: yield any unpaired yielded values from other iterables
81
+ :return: iterator of merged values from the iterables until one is exhausted
82
+
83
+ """
84
+ iter_list = list(map(lambda x: iter(x), iterables))
85
+ values = []
86
+ if (num_iters := len(iter_list)) > 0:
87
+ while True:
88
+ try:
89
+ for ii in range(num_iters):
90
+ values.append(next(iter_list[ii]))
91
+ yield from values
92
+ values.clear()
93
+ except StopIteration:
94
+ break
95
+ if yield_partials:
96
+ yield from values
97
+
98
+
99
+ def exhaust[D](*iterables: Iterable[D]) -> Iterator[D]:
100
+ """Merge together multiple iterables until all are exhausted.
101
+
102
+ :param iterables: iterables to exhaustively merge
103
+ :return: iterator of merged values from the iterables until all are exhausted
104
+
105
+ """
106
+ iter_list = list(map(lambda x: iter(x), iterables))
107
+ if (num_iters := len(iter_list)) > 0:
108
+ ii = 0
109
+ values = []
110
+ while True:
111
+ try:
112
+ while ii < num_iters:
113
+ values.append(next(iter_list[ii]))
114
+ ii += 1
115
+ yield from values
116
+ ii = 0
117
+ values.clear()
118
+ except StopIteration:
119
+ num_iters -= 1
120
+ if num_iters < 1:
121
+ break
122
+ del iter_list[ii]
123
+
124
+ yield from values
125
+
126
+
127
+ def blend[D](*iterables: Iterable[D],
128
+ merge_enum: MergeEnum = MergeEnum.Concat,
129
+ yield_partials: bool = False
130
+ ) -> Iterator[D]:
131
+ """Base merge behavior on name only parameters.
132
+
133
+ :param iterables: iterables to blend together
134
+ :param merge_enum: MergeEnum to determine merging behavior
135
+ :param yield_partials: yield yielded unpaired values from other iterables
136
+ :return: an iterator of type D
137
+ :raises ValueError: when an unknown MergeEnum is given
138
+
139
+ """
140
+ match merge_enum:
141
+ case MergeEnum.Concat:
142
+ return concat(*iterables)
143
+ case MergeEnum.Merge:
144
+ return merge(*iterables, yield_partials = yield_partials)
145
+ case MergeEnum.Exhaust:
146
+ return exhaust(*iterables)
147
+
148
+ raise ValueError('Unknown MergeEnum given')
File without changes
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
- Name: pythonic-fp.iterables
3
- Version: 3.0.0
4
- Summary: Module fp.iterables - Iterator related tools
2
+ Name: pythonic-fp-iterables
3
+ Version: 5.0.0
4
+ Summary: Library of functions for iterating iterables.
5
5
  Keywords: iterables,iterators
6
6
  Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
7
7
  Requires-Python: >=3.12
8
- Description-Content-Type: text/markdown
8
+ Description-Content-Type: text/x-rst
9
9
  Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Framework :: Pytest
11
11
  Classifier: Intended Audience :: Developers
@@ -14,38 +14,47 @@ Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Typing :: Typed
16
16
  License-File: LICENSE
17
- Requires-Dist: pythonic-fp.circulararray>=5.0.0
18
- Requires-Dist: pythonic-fp.containers >=2.0.0
19
- Requires-Dist: pythonic-fp.fptools >=3.0.0
20
- Project-URL: Changelog, https://grscheller.github.io/pythonic-fp-iterables/html/changelog.html
21
- Project-URL: Documentation, https://grscheller.github.io/pythonic-fp-iterables/html/
17
+ Requires-Dist: pythonic-fp-fptools>=5.0.0
18
+ Requires-Dist: pythonic-fp>=1.1.0
19
+ Requires-Dist: pythonic-fp-singletons>=1.0.0
20
+ Requires-Dist: pytest>=8.4.1 ; extra == "test"
21
+ Project-URL: Changelog, https://github.com/grscheller/pythonic-fp-iterables/blob/main/CHANGELOG.rst
22
+ Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/iterables/development/build/html/releases.html
22
23
  Project-URL: Homepage, https://github.com/grscheller/pythonic-fp/blob/main/README.md
23
24
  Project-URL: Source, https://github.com/grscheller/pythonic-fp-iterables
25
+ Provides-Extra: test
24
26
 
25
- # Pythonic FP - Tools for iterables
27
+ Pythonic FP - Tools for iterables
28
+ =================================
26
29
 
27
30
  PyPI project
28
- [pythonic-fp.iterators](https://pypi.org/project/pythonic-fp.iterables/)
29
- implementing a Python package of tools for iterables and iterators.
31
+ `pythonic-fp.iterators <https://pypi.org/project/pythonic-fp.iterables>`_.
32
+
33
+ Library of functions for iterating iterables.
30
34
 
31
35
  - Concatenating and merging iterables
32
36
  - Dropping and taking values from iterables
33
37
  - Reducing and accumulating iterables
34
38
  - Assumptions
39
+
35
40
  - iterables are not necessarily iterators
36
41
  - at all times iterator protocol is assumed to be followed
42
+
37
43
  - all iterators are assumed to be iterable
38
- - for all iterators `my_iter` we assume `iter(my_iter) is my_iter`
44
+ - for all iterators ``foo`` we assume ``iter(foo) is foo``
39
45
 
40
- This PyPI project is part of of the grscheller
41
- [pythonic-fp namespace projects](https://github.com/grscheller/pythonic-fp/blob/main/README.md).
46
+ This PyPI project is part of of the
47
+ `pythonic-fp namespace projects <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
42
48
 
43
- ## Documentation
49
+ Documentation
50
+ -------------
44
51
 
45
- Documentation hosted on
46
- [GitHub Pages](https://grscheller.github.io/pythonic-fp-iterables/html).
52
+ Documentation for this project is hosted on
53
+ `GitHub Pages
54
+ <https://grscheller.github.io/pythonic-fp/iterables/development/build/html>`_.
47
55
 
48
- ## Copyright and License
56
+ Copyright and License
57
+ ---------------------
49
58
 
50
59
  Copyright (c) 2023-2025 Geoffrey R. Scheller. Licensed under the Apache
51
60
  License, Version 2.0. See the LICENSE file for details.
@@ -0,0 +1,9 @@
1
+ pythonic_fp/iterables/__init__.py,sha256=gtTAk0cCZ4kjfx54hZcwURNXYg02BNr3nKcOP5gdJEk,1534
2
+ pythonic_fp/iterables/drop_take.py,sha256=70K6q0DGumK0XHFgPCpLK-lLDfNTkBzzjb2P5uttEqM,4940
3
+ pythonic_fp/iterables/folding.py,sha256=o3m7MNCWtlPoUsQUrXJ-xhrGsb3Nxp-iJJ7xJE5bbo0,8925
4
+ pythonic_fp/iterables/merging.py,sha256=mhFgSuIFb64wq9gnNkk63zoh92T7lkWB16VQ2L8t3-4,4530
5
+ pythonic_fp/iterables/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pythonic_fp_iterables-5.0.0.dist-info/licenses/LICENSE,sha256=IJMkVWQUVy-SonK6SMaMotDQ6Zb-lVi2bUTiGp1GUO8,10774
7
+ pythonic_fp_iterables-5.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
8
+ pythonic_fp_iterables-5.0.0.dist-info/METADATA,sha256=kFDuTmQMieyCjmD-KmXzZy5970iL39BV-QXMEGaDcxA,2212
9
+ pythonic_fp_iterables-5.0.0.dist-info/RECORD,,
pythonic_fp/iterables.py DELETED
@@ -1,455 +0,0 @@
1
- # Copyright 2023-2025 Geoffrey R. Scheller
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
-
15
- """Module fp.iterables - Iterator related tools
16
-
17
- Library of iterator related functions and enumerations.
18
-
19
- - Concatenating and merging iterables
20
- - Dropping and taking values from iterables
21
- - Reducing and accumulating iterables
22
- - Assumptions
23
-
24
- - iterables are not necessarily iterators
25
- - at all times iterator protocol is assumed to be followed
26
-
27
- - all iterators are assumed to be iterable
28
- - for all iterators ``foo`` we assume ``iter(foo) is foo``
29
- """
30
-
31
- from __future__ import annotations
32
-
33
- __author__ = 'Geoffrey R. Scheller'
34
- __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
35
- __license__ = 'Apache License 2.0'
36
-
37
- from collections.abc import Callable, Iterable, Iterator
38
- from enum import auto, Enum
39
- from typing import cast, Never, TypeVar
40
- from pythonic_fp.containers.box import Box
41
- from pythonic_fp.containers.maybe import MayBe as MB
42
- from pythonic_fp.fptools.function import negate, swap
43
- from pythonic_fp.fptools.singletons import NoValue
44
-
45
- __all__ = [
46
- 'FM',
47
- 'concat',
48
- 'merge',
49
- 'exhaust',
50
- 'drop',
51
- 'drop_while',
52
- 'take',
53
- 'take_while',
54
- 'take_split',
55
- 'take_while_split',
56
- 'accumulate',
57
- 'foldl',
58
- 'mb_fold_left',
59
- 'reduce_left',
60
- 'sc_reduce_left',
61
- 'sc_reduce_right',
62
- ]
63
-
64
- D = TypeVar('D') # Needed only for pdoc documentation generation.
65
- L = TypeVar('L') # Otherwise, ignored by both MyPy and Python.
66
-
67
- # Iterate over multiple iterables
68
-
69
-
70
- class FM(Enum):
71
- """Types of iterable blending,
72
-
73
- - **CONCAT:** Concatenate first to last
74
- - **MERGE:** Merge until one is exhausted
75
- - **EXHAUST:** Merge until all are exhausted
76
- """
77
-
78
- CONCAT = auto()
79
- MERGE = auto()
80
- EXHAUST = auto()
81
-
82
-
83
- def concat[D](*iterables: Iterable[D]) -> Iterator[D]:
84
- """Sequentially concatenate multiple iterables together.
85
-
86
- - pure Python version of standard library's ``itertools.chain``
87
- - iterator sequentially yields each iterable until all are exhausted
88
- - an infinite iterable will prevent subsequent iterables from yielding any values
89
- - performant to ``itertools.chain``
90
- """
91
- for iterator in map(lambda x: iter(x), iterables):
92
- while True:
93
- try:
94
- value = next(iterator)
95
- yield value
96
- except StopIteration:
97
- break
98
-
99
-
100
- def exhaust[D](*iterables: Iterable[D]) -> Iterator[D]:
101
- """Shuffle together multiple iterables until all are exhausted.
102
- Iterator yields until all iterables are exhausted.
103
- """
104
- iter_list = list(map(lambda x: iter(x), iterables))
105
- if (num_iters := len(iter_list)) > 0:
106
- ii = 0
107
- values = []
108
- while True:
109
- try:
110
- while ii < num_iters:
111
- values.append(next(iter_list[ii]))
112
- ii += 1
113
- yield from values
114
- ii = 0
115
- values.clear()
116
- except StopIteration:
117
- num_iters -= 1
118
- if num_iters < 1:
119
- break
120
- del iter_list[ii]
121
-
122
- yield from values
123
-
124
-
125
- def merge[D](*iterables: Iterable[D], yield_partials: bool = False) -> Iterator[D]:
126
- """Shuffle together the ``iterables`` until one is exhausted.
127
-
128
- - iterator yields until one of the iterables is exhausted
129
- - if ``yield_partials`` is true,
130
-
131
- - yield any unmatched yielded values from other iterables
132
- - prevents data lose
133
-
134
- - if any of the iterables are iterators with external references
135
- """
136
- iter_list = list(map(lambda x: iter(x), iterables))
137
- values = []
138
- if (num_iters := len(iter_list)) > 0:
139
- while True:
140
- try:
141
- for ii in range(num_iters):
142
- values.append(next(iter_list[ii]))
143
- yield from values
144
- values.clear()
145
- except StopIteration:
146
- break
147
- if yield_partials:
148
- yield from values
149
-
150
-
151
- ## dropping and taking
152
-
153
-
154
- def drop[D](iterable: Iterable[D], n: int, /) -> Iterator[D]:
155
- """Drop the next ``n`` values from ``iterable``."""
156
- it = iter(iterable)
157
- for _ in range(n):
158
- try:
159
- next(it)
160
- except StopIteration:
161
- break
162
- return it
163
-
164
-
165
- def drop_while[D](iterable: Iterable[D], pred: Callable[[D], bool], /) -> Iterator[D]:
166
- """Drop initial values from ``iterable`` while predicate is true."""
167
- it = iter(iterable)
168
- while True:
169
- try:
170
- value = next(it)
171
- if not pred(value):
172
- it = concat((value,), it)
173
- break
174
- except StopIteration:
175
- break
176
- return it
177
-
178
-
179
- def take[D](iterable: Iterable[D], n: int, /) -> Iterator[D]:
180
- """Return an iterator of up to ``n`` initial values of an iterable"""
181
- it = iter(iterable)
182
- for _ in range(n):
183
- try:
184
- value = next(it)
185
- yield value
186
- except StopIteration:
187
- break
188
-
189
-
190
- def take_split[D](iterable: Iterable[D], n: int, /) -> tuple[Iterator[D], Iterator[D]]:
191
- """Same as take except also return an iterator of the remaining values.
192
-
193
- - return a tuple of
194
-
195
- - an iterator of up to ``n`` initial values
196
- - an iterator of the remaining vales of the ``iterable``
197
-
198
- - Contract: do not access second iterator until first is exhausted
199
- """
200
- it = iter(iterable)
201
- itn = take(it, n)
202
-
203
- return itn, it
204
-
205
-
206
- def take_while[D](iterable: Iterable[D], pred: Callable[[D], bool], /) -> Iterator[D]:
207
- """Yield values from ``iterable`` while predicate is true.
208
-
209
- .. warning::
210
- Risk of value loss if iterable is multiple referenced iterator.
211
- """
212
- it = iter(iterable)
213
- while True:
214
- try:
215
- value = next(it)
216
- if pred(value):
217
- yield value
218
- else:
219
- break
220
- except StopIteration:
221
- break
222
-
223
-
224
- def take_while_split[D](
225
- iterable: Iterable[D], pred: Callable[[D], bool], /
226
- ) -> tuple[Iterator[D], Iterator[D]]:
227
- """Yield values from ``iterable`` while ``predicate`` is true.
228
-
229
- - return a tuple of two iterators
230
-
231
- - first of initial values where predicate is true, followed by first to fail
232
- - second of the remaining values of the iterable after first failed value
233
-
234
- - **Contract:** do not access second iterator until first is exhausted
235
- """
236
-
237
- def _take_while(
238
- it: Iterator[D], pred: Callable[[D], bool], val: Box[D]
239
- ) -> Iterator[D]:
240
- while True:
241
- try:
242
- val.put(next(it))
243
- if pred(val.get()):
244
- yield val.pop()
245
- else:
246
- break
247
- except StopIteration:
248
- break
249
-
250
- it = iter(iterable)
251
- value: Box[D] = Box()
252
- it_pred = _take_while(it, pred, value)
253
-
254
- return (it_pred, concat(value, it))
255
-
256
-
257
- ## reducing and accumulating
258
-
259
-
260
- def accumulate[D, L](
261
- iterable: Iterable[D], f: Callable[[L, D], L], initial: L | NoValue = NoValue(), /
262
- ) -> Iterator[L]:
263
- """Returns an iterator of accumulated values.
264
-
265
- - pure Python version of standard library's ``itertools.accumulate``
266
- - function ``f`` does not default to addition (for typing flexibility)
267
- - begins accumulation with an optional ``initial`` value
268
- """
269
- it = iter(iterable)
270
- try:
271
- it0 = next(it)
272
- except StopIteration:
273
- if initial is NoValue():
274
- return
275
- yield cast(L, initial)
276
- else:
277
- if initial is not NoValue():
278
- init = cast(L, initial)
279
- yield init
280
- acc = f(init, it0)
281
- for ii in it:
282
- yield acc
283
- acc = f(acc, ii)
284
- yield acc
285
- else:
286
- acc = cast(L, it0) # in this case L = D
287
- for ii in it:
288
- yield acc
289
- acc = f(acc, ii)
290
- yield acc
291
-
292
-
293
- def reduce_left[D](iterable: Iterable[D], f: Callable[[D, D], D], /) -> D | Never:
294
- """Fold an iterable left with a function.
295
-
296
- - traditional FP type order given for function ``f``
297
- - if iterable empty, ``StopIteration`` exception raised
298
- - does not catch any exceptions ``f`` may raise
299
- - never returns if ``iterable`` generates an infinite iterator
300
- """
301
- it = iter(iterable)
302
- try:
303
- acc = next(it)
304
- except StopIteration as exc:
305
- msg = 'Attemped to reduce an empty iterable.'
306
- raise StopIteration(msg) from exc
307
-
308
- for v in it:
309
- acc = f(acc, v)
310
-
311
- return acc
312
-
313
-
314
- def foldl[D, L](
315
- iterable: Iterable[D], f: Callable[[L, D], L], initial: L, /
316
- ) -> L | Never:
317
- """Folds an iterable left with a function and initial value.
318
-
319
- - traditional FP type order given for function ``f``
320
- - does not catch any exceptions ``f`` may raise
321
- - like builtin ``sum`` for Python >=3.8 except
322
-
323
- - not restricted to ``__add__`` for the folding function
324
- - initial value required, does not default to ``0`` for initial value
325
- - handles non-numeric data just find
326
-
327
- - never returns if ``iterable`` generates an infinite iterator
328
- """
329
- acc = initial
330
- for v in iterable:
331
- acc = f(acc, v)
332
- return acc
333
-
334
-
335
- def mb_fold_left[L, D](
336
- iterable: Iterable[D], f: Callable[[L, D], L], initial: L | NoValue = NoValue()
337
- ) -> MB[L]:
338
- """Folds an iterable left with optional initial value.
339
-
340
- - traditional FP type order given for function ``f``
341
- - when an initial value is not given then ``~L = ~D``
342
- - if iterable empty and no ``initial`` value given, return ``MB()``
343
- - never returns if iterable generates an infinite iterator
344
- """
345
- acc: L
346
- it = iter(iterable)
347
- if initial is NoValue():
348
- try:
349
- acc = cast(L, next(it)) # in this case L = D
350
- except StopIteration:
351
- return MB()
352
- else:
353
- acc = cast(L, initial)
354
-
355
- for v in it:
356
- try:
357
- acc = f(acc, v)
358
- except Exception:
359
- return MB()
360
-
361
- return MB(acc)
362
-
363
-
364
- def sc_reduce_left[D](
365
- iterable: Iterable[D],
366
- f: Callable[[D, D], D],
367
- /,
368
- start: Callable[[D], bool] = (lambda d: True),
369
- stop: Callable[[D], bool] = (lambda d: False),
370
- include_start: bool = True,
371
- include_stop: bool = True,
372
- ) -> tuple[MB[D], Iterator[D]]:
373
- """Short circuit version of a left reduce. Useful for infinite or iterables.
374
-
375
- - Behavior for default arguments will
376
-
377
- - left reduce finite iterable
378
- - start folding immediately
379
- - continue folding until end (of a possibly infinite iterable)
380
-
381
- - Callable ``start`` delays starting the left reduce
382
- - Callable ``stop`` prematurely stop the left reduce
383
- """
384
- it_start = drop_while(iterable, negate(start))
385
- if not include_start:
386
- try:
387
- next(it_start)
388
- except StopIteration:
389
- pass
390
- it_reduce, it_rest = take_while_split(it_start, negate(stop))
391
- mb_reduced = mb_fold_left(it_reduce, f)
392
- if include_stop:
393
- if mb_reduced:
394
- try:
395
- last = next(it_rest)
396
- mb_reduced = MB(f(mb_reduced.get(), last))
397
- except StopIteration:
398
- pass
399
- else:
400
- try:
401
- last = next(it_rest)
402
- mb_reduced = MB(last)
403
- except StopIteration:
404
- pass
405
-
406
- return (mb_reduced, it_rest)
407
-
408
-
409
- def sc_reduce_right[D](
410
- iterable: Iterable[D],
411
- f: Callable[[D, D], D],
412
- /,
413
- start: Callable[[D], bool] = (lambda d: False),
414
- stop: Callable[[D], bool] = (lambda d: False),
415
- include_start: bool = True,
416
- include_stop: bool = True,
417
- ) -> tuple[MB[D], Iterator[D]]:
418
- """Short circuit version of a right reduce. Useful for infinite or
419
- non-reversible iterables.
420
-
421
- - Behavior for default arguments will
422
-
423
- - right reduce finite iterable
424
- - start reducing at end (of a possibly infinite iterable)
425
- - continue reducing right until beginning
426
-
427
- - Callable ``start`` prematurely starts the right reduce
428
- - Callable ``stop`` prematurely stops the right reduce
429
- """
430
- it_start, it_rest = take_while_split(iterable, negate(start))
431
- l1 = list(it_start)
432
- if include_start:
433
- try:
434
- begin = next(it_rest)
435
- except StopIteration:
436
- pass
437
- else:
438
- l1.append(begin)
439
-
440
- l1.reverse()
441
- it_reduce, it_stop = take_while_split(l1, negate(stop))
442
-
443
- mb_reduced = mb_fold_left(it_reduce, swap(f))
444
- if include_stop:
445
- try:
446
- end = next(it_stop)
447
- except StopIteration:
448
- pass
449
- else:
450
- if mb_reduced:
451
- mb_reduced = MB(f(end, mb_reduced.get()))
452
- else:
453
- mb_reduced = MB(end)
454
-
455
- return (mb_reduced, it_rest)
@@ -1,5 +0,0 @@
1
- pythonic_fp/iterables.py,sha256=Si4a4JcJhrStvpxqe8msaqiskcgrb3D8xJP2oN-6VG8,13194
2
- pythonic_fp_iterables-3.0.0.dist-info/licenses/LICENSE,sha256=IJMkVWQUVy-SonK6SMaMotDQ6Zb-lVi2bUTiGp1GUO8,10774
3
- pythonic_fp_iterables-3.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
4
- pythonic_fp_iterables-3.0.0.dist-info/METADATA,sha256=9vZd4-jSvYbAXdXXSo6UwZHcLgrSEprn2J6cgyqUkeA,2059
5
- pythonic_fp_iterables-3.0.0.dist-info/RECORD,,