rangeslib 0.6.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.
rangeslib/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from . import ranges, views
2
+ from ._core import Range
3
+
4
+ __all__ = [
5
+ "Range",
6
+ "ranges",
7
+ "views",
8
+ ]
rangeslib/_adaptors.py ADDED
@@ -0,0 +1,396 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import islice, product
4
+ from typing import Callable, Iterable, Protocol, cast
5
+
6
+ from ._core import Range, RangeAdaptor
7
+
8
+ type Pattern[InputT] = InputT | Iterable[InputT]
9
+
10
+
11
+ def _pattern_from[InputT](value: Pattern[InputT]) -> tuple[InputT, ...]:
12
+ try:
13
+ return tuple(cast(Iterable[InputT], value))
14
+ except TypeError:
15
+ return (cast(InputT, value),)
16
+
17
+
18
+ def _starts_with[InputT](
19
+ values: list[InputT], index: int, pattern: tuple[InputT, ...]
20
+ ) -> bool:
21
+ return tuple(values[index : index + len(pattern)]) == pattern
22
+
23
+
24
+ class SupportsGetItem(Protocol):
25
+ """Structural requirement for tuple-like indexed elements."""
26
+
27
+ def __getitem__(self, index: int, /) -> object: ...
28
+
29
+
30
+ class All[InputT](RangeAdaptor[InputT, Range[InputT]]):
31
+ """Materialize any iterable as an eager ``Range``."""
32
+
33
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
34
+ return Range(*iterable)
35
+
36
+
37
+ class To[InputT, OutputT](RangeAdaptor[InputT, OutputT]):
38
+ """Convert an iterable with a supplied collection or factory callable."""
39
+
40
+ def __init__(self, target_type: Callable[[Iterable[InputT]], OutputT]) -> None:
41
+ self.target_type = target_type
42
+
43
+ def __call__(self, iterable: Iterable[InputT]) -> OutputT:
44
+ return self.target_type(iterable)
45
+
46
+
47
+ class Reverse[InputT](RangeAdaptor[InputT, Range[InputT]]):
48
+ """Return the input elements in reverse order."""
49
+
50
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
51
+ return Range(*reversed(list(iterable)))
52
+
53
+
54
+ class Filter[InputT](RangeAdaptor[InputT, Range[InputT]]):
55
+ """Keep elements for which ``predicate`` returns true."""
56
+
57
+ def __init__(self, predicate: Callable[[InputT], bool]) -> None:
58
+ self.predicate = predicate
59
+
60
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
61
+ return Range(*filter(self.predicate, iterable))
62
+
63
+
64
+ class Transform[InputT, OutputT](RangeAdaptor[InputT, Range[OutputT]]):
65
+ """Map each input element to a new value with ``func``."""
66
+
67
+ def __init__(self, func: Callable[[InputT], OutputT]) -> None:
68
+ self.func = func
69
+
70
+ def __call__(self, iterable: Iterable[InputT]) -> Range[OutputT]:
71
+ return Range(*map(self.func, iterable))
72
+
73
+
74
+ class Take[InputT](RangeAdaptor[InputT, Range[InputT]]):
75
+ """Apply Python slice-stop semantics to the input."""
76
+
77
+ def __init__(self, n: int) -> None:
78
+ self.n = n
79
+
80
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
81
+ if self.n >= 0:
82
+ return Range(*islice(iterable, self.n))
83
+ return Range(*list(iterable)[: self.n])
84
+
85
+
86
+ class TakeWhile[InputT](RangeAdaptor[InputT, Range[InputT]]):
87
+ """Keep the initial elements while ``predicate`` remains true."""
88
+
89
+ def __init__(self, predicate: Callable[[InputT], bool]) -> None:
90
+ self.predicate = predicate
91
+
92
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
93
+ result: list[InputT] = []
94
+ for value in iterable:
95
+ if not self.predicate(value):
96
+ break
97
+ result.append(value)
98
+ return Range(*result)
99
+
100
+
101
+ class Drop[InputT](RangeAdaptor[InputT, Range[InputT]]):
102
+ """Apply Python slice-start semantics to the input."""
103
+
104
+ def __init__(self, n: int) -> None:
105
+ self.n = n
106
+
107
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
108
+ return Range(*list(iterable)[self.n :])
109
+
110
+
111
+ class DropWhile[InputT](RangeAdaptor[InputT, Range[InputT]]):
112
+ """Discard the initial elements while ``predicate`` remains true."""
113
+
114
+ def __init__(self, predicate: Callable[[InputT], bool]) -> None:
115
+ self.predicate = predicate
116
+
117
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
118
+ result: list[InputT] = []
119
+ dropping = True
120
+ for value in iterable:
121
+ if dropping and not self.predicate(value):
122
+ dropping = False
123
+ if not dropping:
124
+ result.append(value)
125
+ return Range(*result)
126
+
127
+
128
+ class Counted[InputT](RangeAdaptor[InputT, Range[InputT]]):
129
+ """Take a bounded prefix from the input's current iterator position."""
130
+
131
+ def __init__(self, count: int) -> None:
132
+ if count < 0:
133
+ raise ValueError("Counted count cannot be negative")
134
+ self.count = count
135
+
136
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
137
+ return Range(*islice(iterable, self.count))
138
+
139
+
140
+ class Elements[OutputT](RangeAdaptor[SupportsGetItem, Range[OutputT]]):
141
+ """Project one integer-indexed field from each tuple-like element."""
142
+
143
+ def __init__(self, index: int) -> None:
144
+ self.index = index
145
+
146
+ def __call__(self, iterable: Iterable[SupportsGetItem]) -> Range[OutputT]:
147
+ return Range(*(cast(OutputT, value[self.index]) for value in iterable))
148
+
149
+
150
+ class Keys[OutputT](Elements[OutputT]):
151
+ """Project index ``0`` from each tuple-like element."""
152
+
153
+ def __init__(self) -> None:
154
+ super().__init__(0)
155
+
156
+
157
+ class Values[OutputT](Elements[OutputT]):
158
+ """Project index ``1`` from each tuple-like element."""
159
+
160
+ def __init__(self) -> None:
161
+ super().__init__(1)
162
+
163
+
164
+ class Enumerate[InputT](RangeAdaptor[InputT, Range[tuple[int, InputT]]]):
165
+ """Pair each input value with its sequential index."""
166
+
167
+ def __init__(self, start: int = 0) -> None:
168
+ self.start = start
169
+
170
+ def __call__(self, iterable: Iterable[InputT]) -> Range[tuple[int, InputT]]:
171
+ return Range(*enumerate(iterable, self.start))
172
+
173
+
174
+ class Concat[InputT](RangeAdaptor[InputT, Range[InputT]]):
175
+ """Append configured iterables after the piped input."""
176
+
177
+ def __init__(self, *iterables: Iterable[InputT]) -> None:
178
+ self.iterables = iterables
179
+
180
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
181
+ result: list[InputT] = list(iterable)
182
+ for additional_iterable in self.iterables:
183
+ result.extend(additional_iterable)
184
+ return Range(*result)
185
+
186
+
187
+ class Zip[InputT](RangeAdaptor[InputT, Range[tuple[InputT, ...]]]):
188
+ """Zip the piped input with additional iterables to the shortest length."""
189
+
190
+ def __init__(self, *iterables: Iterable[InputT]) -> None:
191
+ self.iterables = iterables
192
+
193
+ def __call__(self, iterable: Iterable[InputT]) -> Range[tuple[InputT, ...]]:
194
+ return Range(*zip(iterable, *self.iterables))
195
+
196
+
197
+ class ZipTransform[OutputT](RangeAdaptor[object, Range[OutputT]]):
198
+ """Apply a callable to corresponding values from several iterables."""
199
+
200
+ def __init__(
201
+ self, func: Callable[..., OutputT], *iterables: Iterable[object]
202
+ ) -> None:
203
+ self.func = func
204
+ self.iterables = iterables
205
+
206
+ def __call__(self, iterable: Iterable[object]) -> Range[OutputT]:
207
+ return Range(*(self.func(*values) for values in zip(iterable, *self.iterables)))
208
+
209
+
210
+ class Adjacent[InputT](RangeAdaptor[InputT, Range[tuple[InputT, ...]]]):
211
+ """Return overlapping windows of ``width`` adjacent elements."""
212
+
213
+ def __init__(self, width: int = 2) -> None:
214
+ if width < 1:
215
+ raise ValueError("Adjacent width must be positive")
216
+ self.width = width
217
+
218
+ def __call__(self, iterable: Iterable[InputT]) -> Range[tuple[InputT, ...]]:
219
+ values = list(iterable)
220
+ return Range(
221
+ *(
222
+ tuple(values[index : index + self.width])
223
+ for index in range(len(values) - self.width + 1)
224
+ )
225
+ )
226
+
227
+
228
+ class Pairwise[InputT](RangeAdaptor[InputT, Range[tuple[InputT, InputT]]]):
229
+ """Return overlapping two-element windows."""
230
+
231
+ def __call__(self, iterable: Iterable[InputT]) -> Range[tuple[InputT, InputT]]:
232
+ values = list(iterable)
233
+ return Range(
234
+ *((values[index], values[index + 1]) for index in range(len(values) - 1))
235
+ )
236
+
237
+
238
+ class AdjacentTransform[InputT, OutputT](RangeAdaptor[InputT, Range[OutputT]]):
239
+ """Apply a callable to each overlapping adjacent window."""
240
+
241
+ def __init__(self, func: Callable[..., OutputT], width: int = 2) -> None:
242
+ if width < 1:
243
+ raise ValueError("AdjacentTransform width must be positive")
244
+ self.func = func
245
+ self.width = width
246
+
247
+ def __call__(self, iterable: Iterable[InputT]) -> Range[OutputT]:
248
+ values = list(iterable)
249
+ return Range(
250
+ *(
251
+ self.func(*values[index : index + self.width])
252
+ for index in range(len(values) - self.width + 1)
253
+ )
254
+ )
255
+
256
+
257
+ class PairwiseTransform[InputT, OutputT](AdjacentTransform[InputT, OutputT]):
258
+ """Apply a binary callable to each pair of adjacent elements."""
259
+
260
+ def __init__(self, func: Callable[[InputT, InputT], OutputT]) -> None:
261
+ super().__init__(func, 2)
262
+
263
+
264
+ class Chunk[InputT](RangeAdaptor[InputT, Range[Range[InputT]]]):
265
+ """Partition input into non-overlapping chunks of up to ``size`` elements."""
266
+
267
+ def __init__(self, size: int) -> None:
268
+ if size < 1:
269
+ raise ValueError("Chunk size must be positive")
270
+ self.size = size
271
+
272
+ def __call__(self, iterable: Iterable[InputT]) -> Range[Range[InputT]]:
273
+ values = list(iterable)
274
+ return Range(
275
+ *(
276
+ Range(*values[index : index + self.size])
277
+ for index in range(0, len(values), self.size)
278
+ )
279
+ )
280
+
281
+
282
+ class Slide[InputT](RangeAdaptor[InputT, Range[Range[InputT]]]):
283
+ """Return overlapping windows of ``width`` elements."""
284
+
285
+ def __init__(self, width: int) -> None:
286
+ if width < 1:
287
+ raise ValueError("Slide width must be positive")
288
+ self.width = width
289
+
290
+ def __call__(self, iterable: Iterable[InputT]) -> Range[Range[InputT]]:
291
+ values = list(iterable)
292
+ return Range(
293
+ *(
294
+ Range(*values[index : index + self.width])
295
+ for index in range(len(values) - self.width + 1)
296
+ )
297
+ )
298
+
299
+
300
+ class ChunkBy[InputT](RangeAdaptor[InputT, Range[Range[InputT]]]):
301
+ """Split input when the adjacent-value predicate returns false."""
302
+
303
+ def __init__(self, predicate: Callable[[InputT, InputT], bool]) -> None:
304
+ self.predicate = predicate
305
+
306
+ def __call__(self, iterable: Iterable[InputT]) -> Range[Range[InputT]]:
307
+ values = list(iterable)
308
+ if not values:
309
+ return Range()
310
+
311
+ chunks: list[Range[InputT]] = []
312
+ current_chunk = [values[0]]
313
+ for previous, value in zip(values, values[1:]):
314
+ if self.predicate(previous, value):
315
+ current_chunk.append(value)
316
+ else:
317
+ chunks.append(Range(*current_chunk))
318
+ current_chunk = [value]
319
+ chunks.append(Range(*current_chunk))
320
+ return Range(*chunks)
321
+
322
+
323
+ class Stride[InputT](RangeAdaptor[InputT, Range[InputT]]):
324
+ """Select every ``step``-th input element, starting with the first."""
325
+
326
+ def __init__(self, step: int) -> None:
327
+ if step < 1:
328
+ raise ValueError("Stride step must be positive")
329
+ self.step = step
330
+
331
+ def __call__(self, iterable: Iterable[InputT]) -> Range[InputT]:
332
+ return Range(*islice(iterable, 0, None, self.step))
333
+
334
+
335
+ class CartesianProduct[InputT](RangeAdaptor[InputT, Range[tuple[InputT, ...]]]):
336
+ """Return the Cartesian product of the input and configured iterables."""
337
+
338
+ def __init__(self, *iterables: Iterable[InputT]) -> None:
339
+ self.iterables = iterables
340
+
341
+ def __call__(self, iterable: Iterable[InputT]) -> Range[tuple[InputT, ...]]:
342
+ return Range(*product(iterable, *self.iterables))
343
+
344
+
345
+ class Join[InputT](RangeAdaptor[Iterable[InputT], Range[InputT]]):
346
+ """Flatten one level of nested iterables."""
347
+
348
+ def __call__(self, iterable: Iterable[Iterable[InputT]]) -> Range[InputT]:
349
+ result: list[InputT] = []
350
+ for sub_iterable in iterable:
351
+ result.extend(sub_iterable)
352
+ return Range(*result)
353
+
354
+
355
+ class JoinWith[InputT](RangeAdaptor[Iterable[InputT], Range[InputT]]):
356
+ """Flatten nested iterables with a separator pattern between them."""
357
+
358
+ def __init__(self, separator: Pattern[InputT]) -> None:
359
+ self.separator = _pattern_from(separator)
360
+
361
+ def __call__(self, iterable: Iterable[Iterable[InputT]]) -> Range[InputT]:
362
+ result: list[InputT] = []
363
+ first = True
364
+ for sub_iterable in iterable:
365
+ if not first:
366
+ result.extend(self.separator)
367
+ result.extend(sub_iterable)
368
+ first = False
369
+ return Range(*result)
370
+
371
+
372
+ class Split[InputT](RangeAdaptor[InputT, Range[Range[InputT]]]):
373
+ """Split input into ranges wherever a separator pattern occurs."""
374
+
375
+ def __init__(self, separator: Pattern[InputT]) -> None:
376
+ self.separator = _pattern_from(separator)
377
+
378
+ def __call__(self, iterable: Iterable[InputT]) -> Range[Range[InputT]]:
379
+ separator_length = len(self.separator)
380
+ if separator_length == 0:
381
+ raise ValueError("Split separator cannot be empty")
382
+
383
+ values = list(iterable)
384
+ result: list[Range[InputT]] = []
385
+ current_chunk: list[InputT] = []
386
+ index = 0
387
+ while index < len(values):
388
+ if _starts_with(values, index, self.separator):
389
+ result.append(Range(*current_chunk))
390
+ current_chunk = []
391
+ index += separator_length
392
+ else:
393
+ current_chunk.append(values[index])
394
+ index += 1
395
+ result.append(Range(*current_chunk))
396
+ return Range(*result)
rangeslib/_core.py ADDED
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections import UserList
5
+ from typing import Callable, Iterable, SupportsIndex, overload
6
+
7
+
8
+ class RangeAdaptor[InputT, OutputT](ABC):
9
+ """Base contract for callable transformations over iterable values.
10
+
11
+ Adaptors can be called directly or placed on the right side of ``|``.
12
+ The reflected operator lets built-in iterables such as ``list``, ``str``,
13
+ and ``range`` start a pipeline even though they cannot be modified.
14
+ """
15
+
16
+ def __ror__(self, iterable: Iterable[InputT]) -> OutputT:
17
+ return self(iterable)
18
+
19
+ def __or__[NextOutputT](
20
+ self, adaptor: Callable[[OutputT], NextOutputT], /
21
+ ) -> RangeAdaptor[InputT, NextOutputT]:
22
+ return _ComposedRangeAdaptor(self, adaptor)
23
+
24
+ @abstractmethod
25
+ def __call__(self, iterable: Iterable[InputT]) -> OutputT:
26
+ raise NotImplementedError
27
+
28
+
29
+ class _ComposedRangeAdaptor[InputT, MiddleT, OutputT](RangeAdaptor[InputT, OutputT]):
30
+ def __init__(
31
+ self,
32
+ first: Callable[[Iterable[InputT]], MiddleT],
33
+ second: Callable[[MiddleT], OutputT],
34
+ ) -> None:
35
+ self.first = first
36
+ self.second = second
37
+
38
+ def __call__(self, iterable: Iterable[InputT]) -> OutputT:
39
+ return self.second(self.first(iterable))
40
+
41
+
42
+ class RangeGenerator(ABC):
43
+ """Marker base class for objects that construct :class:`Range` values."""
44
+
45
+
46
+ class Range[T](UserList[T]):
47
+ """A list-backed, typed container used as the library's pipeline value.
48
+
49
+ ``Range`` is eager: constructing or applying an adaptor stores the result
50
+ immediately. Construction uses positional values, so ``Range(1, 2, 3)``
51
+ contains three elements. Standard list-like operations preserve ``Range``
52
+ as the result type.
53
+ """
54
+
55
+ def __init__(self, *args: T) -> None:
56
+ super().__init__(args)
57
+
58
+ def __repr__(self) -> str:
59
+ return f"Range({', '.join(repr(x) for x in self.data)})"
60
+
61
+ def __str__(self) -> str:
62
+ return f"[{', '.join(str(x) for x in self.data)}]"
63
+
64
+ def __or__[OutputT](self, adaptor: Callable[[Iterable[T]], OutputT], /) -> OutputT:
65
+ return adaptor(self)
66
+
67
+ @overload
68
+ def __getitem__(self, index: SupportsIndex) -> T: ...
69
+
70
+ @overload
71
+ def __getitem__(self, index: slice[SupportsIndex | None]) -> Range[T]: ...
72
+
73
+ def __getitem__(
74
+ self, index: SupportsIndex | slice[SupportsIndex | None]
75
+ ) -> T | Range[T]:
76
+ if isinstance(index, slice):
77
+ return Range(*self.data[index])
78
+ return self.data[index]
79
+
80
+ def __add__(self, other: Iterable[T]) -> Range[T]:
81
+ return Range(*self.data, *other)
82
+
83
+ def __radd__(self, other: Iterable[T]) -> Range[T]:
84
+ return Range(*other, *self.data)
85
+
86
+ def __mul__(self, count: int) -> Range[T]:
87
+ return Range(*(self.data * count))
88
+
89
+ def __rmul__(self, count: int) -> Range[T]:
90
+ return self * count
91
+
92
+ def copy(self) -> Range[T]:
93
+ """Return a shallow ``Range`` copy without nesting the source range."""
94
+ return Range(*self.data)
95
+
96
+ def is_empty(self) -> bool:
97
+ """Return ``True`` when the range has no elements."""
98
+ return not self.data
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ # These private factory classes intentionally construct Range instances.
4
+ # Mypy otherwise requires __new__ to return an instance of each factory class.
5
+ # mypy: disable-error-code=misc
6
+
7
+ from ._core import Range, RangeGenerator
8
+
9
+
10
+ class Empty(RangeGenerator):
11
+ """Create an empty integer ``Range``."""
12
+
13
+ def __new__(cls) -> Range[int]:
14
+ return Range()
15
+
16
+
17
+ class Single(RangeGenerator):
18
+ """Create a ``Range`` containing one value."""
19
+
20
+ def __new__[T](cls, value: T) -> Range[T]:
21
+ return Range(value)
22
+
23
+
24
+ class Iota(RangeGenerator):
25
+ """Create integers from ``start`` up to, but excluding, ``end``."""
26
+
27
+ def __new__(cls, start: int, end: int) -> Range[int]:
28
+ return Range(*range(start, end))
29
+
30
+
31
+ class Indices(RangeGenerator):
32
+ """Create zero-based indices from ``0`` up to, but excluding, ``n``."""
33
+
34
+ def __new__(cls, n: int) -> Range[int]:
35
+ return Iota(0, n)
36
+
37
+
38
+ class Repeat(RangeGenerator):
39
+ """Create a ``Range`` containing ``n`` copies of a value."""
40
+
41
+ def __new__[T](cls, value: T, n: int) -> Range[T]:
42
+ return Range(*[value] * n)
rangeslib/py.typed ADDED
File without changes
rangeslib/ranges.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from ._core import Range
4
+ from ._generators import Empty, Indices, Iota, Repeat, Single
5
+
6
+
7
+ def empty() -> Range[int]:
8
+ """Create an empty integer range."""
9
+ return Empty()
10
+
11
+
12
+ def single[T](value: T) -> Range[T]:
13
+ """Create a range containing exactly ``value``."""
14
+ return Single(value)
15
+
16
+
17
+ def iota(start: int, end: int) -> Range[int]:
18
+ """Create integers from ``start`` up to, but excluding, ``end``."""
19
+ return Iota(start, end)
20
+
21
+
22
+ def indices(count: int) -> Range[int]:
23
+ """Create zero-based indices up to, but excluding, ``count``."""
24
+ return Indices(count)
25
+
26
+
27
+ def repeat[T](value: T, count: int) -> Range[T]:
28
+ """Create a range containing ``count`` copies of ``value``."""
29
+ return Repeat(value, count)
30
+
31
+
32
+ __all__ = ["empty", "single", "iota", "indices", "repeat"]
rangeslib/views.py ADDED
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterable, Protocol, cast
4
+
5
+ from ._adaptors import (
6
+ Adjacent,
7
+ AdjacentTransform,
8
+ All,
9
+ CartesianProduct,
10
+ Chunk,
11
+ ChunkBy,
12
+ Concat,
13
+ Counted,
14
+ Drop,
15
+ DropWhile,
16
+ Elements,
17
+ Enumerate,
18
+ Filter,
19
+ Join,
20
+ JoinWith,
21
+ Keys,
22
+ Pairwise,
23
+ PairwiseTransform,
24
+ Reverse,
25
+ Slide,
26
+ Split,
27
+ Stride,
28
+ Take,
29
+ TakeWhile,
30
+ To,
31
+ Transform,
32
+ Values,
33
+ Zip,
34
+ ZipTransform,
35
+ )
36
+ from ._core import Range
37
+
38
+
39
+ class _TypePreservingView(Protocol):
40
+ def __call__[T](self, iterable: Iterable[T], /) -> Range[T]: ...
41
+
42
+ def __ror__[T](self, iterable: Iterable[T], /) -> Range[T]: ...
43
+
44
+
45
+ class _EnumerateView(Protocol):
46
+ def __call__[T](self, iterable: Iterable[T], /) -> Range[tuple[int, T]]: ...
47
+
48
+ def __ror__[T](self, iterable: Iterable[T], /) -> Range[tuple[int, T]]: ...
49
+
50
+
51
+ class _AdjacentView(Protocol):
52
+ def __call__[T](self, iterable: Iterable[T], /) -> Range[tuple[T, ...]]: ...
53
+
54
+ def __ror__[T](self, iterable: Iterable[T], /) -> Range[tuple[T, ...]]: ...
55
+
56
+
57
+ class _PairwiseView(Protocol):
58
+ def __call__[T](self, iterable: Iterable[T], /) -> Range[tuple[T, T]]: ...
59
+
60
+ def __ror__[T](self, iterable: Iterable[T], /) -> Range[tuple[T, T]]: ...
61
+
62
+
63
+ class _ChunkView(Protocol):
64
+ def __call__[T](self, iterable: Iterable[T], /) -> Range[Range[T]]: ...
65
+
66
+ def __ror__[T](self, iterable: Iterable[T], /) -> Range[Range[T]]: ...
67
+
68
+
69
+ class _JoinView(Protocol):
70
+ def __call__[T](self, iterable: Iterable[Iterable[T]], /) -> Range[T]: ...
71
+
72
+ def __ror__[T](self, iterable: Iterable[Iterable[T]], /) -> Range[T]: ...
73
+
74
+
75
+ class _KeysView(Protocol):
76
+ def __call__[KeyT, ValueT](
77
+ self, iterable: Iterable[tuple[KeyT, ValueT]], /
78
+ ) -> Range[KeyT]: ...
79
+
80
+ def __ror__[KeyT, ValueT](
81
+ self, iterable: Iterable[tuple[KeyT, ValueT]], /
82
+ ) -> Range[KeyT]: ...
83
+
84
+
85
+ class _ValuesView(Protocol):
86
+ def __call__[KeyT, ValueT](
87
+ self, iterable: Iterable[tuple[KeyT, ValueT]], /
88
+ ) -> Range[ValueT]: ...
89
+
90
+ def __ror__[KeyT, ValueT](
91
+ self, iterable: Iterable[tuple[KeyT, ValueT]], /
92
+ ) -> Range[ValueT]: ...
93
+
94
+
95
+ class _ZipView[OtherT](Protocol):
96
+ def __call__[InputT](
97
+ self, iterable: Iterable[InputT], /
98
+ ) -> Range[tuple[InputT, OtherT]]: ...
99
+
100
+ def __ror__[InputT](
101
+ self, iterable: Iterable[InputT], /
102
+ ) -> Range[tuple[InputT, OtherT]]: ...
103
+
104
+
105
+ class _CartesianProductView[OtherT](Protocol):
106
+ def __call__[InputT](
107
+ self, iterable: Iterable[InputT], /
108
+ ) -> Range[tuple[InputT, OtherT]]: ...
109
+
110
+ def __ror__[InputT](
111
+ self, iterable: Iterable[InputT], /
112
+ ) -> Range[tuple[InputT, OtherT]]: ...
113
+
114
+
115
+ def all() -> _TypePreservingView:
116
+ """Materialize an existing iterable as an eager ``Range``.
117
+
118
+ This mirrors C++ ``views::all`` at the public API level. Python does not
119
+ expose borrowed-range or view ownership categories, so this implementation
120
+ always returns a reusable, materialized :class:`~rangeslib.Range`.
121
+ """
122
+ return cast(_TypePreservingView, All[object]())
123
+
124
+
125
+ def to[InputT, OutputT](
126
+ target_type: Callable[[Iterable[InputT]], OutputT],
127
+ ) -> To[InputT, OutputT]:
128
+ """Convert the pipeline input with ``target_type``.
129
+
130
+ ``to`` is the only public adaptor that does not necessarily return
131
+ :class:`~rangeslib.Range`; it returns exactly what the supplied callable
132
+ produces.
133
+ """
134
+ return To(target_type)
135
+
136
+
137
+ def reverse() -> _TypePreservingView:
138
+ """Reverse all input values and return an eager ``Range``."""
139
+ return cast(_TypePreservingView, Reverse[object]())
140
+
141
+
142
+ def filter[InputT](predicate: Callable[[InputT], bool]) -> Filter[InputT]:
143
+ """Keep values for which ``predicate`` returns ``True``."""
144
+ return Filter(predicate)
145
+
146
+
147
+ def transform[InputT, OutputT](
148
+ func: Callable[[InputT], OutputT],
149
+ ) -> Transform[InputT, OutputT]:
150
+ """Map every input value through ``func``."""
151
+ return Transform(func)
152
+
153
+
154
+ def take(count: int) -> _TypePreservingView:
155
+ """Take values using Python slice-stop semantics.
156
+
157
+ Positive and zero counts select a prefix. Negative counts behave like
158
+ ``list(iterable)[:count]`` and therefore require complete input
159
+ materialization.
160
+ """
161
+ return cast(_TypePreservingView, Take[object](count))
162
+
163
+
164
+ def takewhile[InputT](predicate: Callable[[InputT], bool]) -> TakeWhile[InputT]:
165
+ """Take initial values while ``predicate`` remains ``True``."""
166
+ return TakeWhile(predicate)
167
+
168
+
169
+ def take_while[InputT](predicate: Callable[[InputT], bool]) -> TakeWhile[InputT]:
170
+ """Alias for :func:`takewhile` using C++-style word separation."""
171
+ return takewhile(predicate)
172
+
173
+
174
+ def drop(count: int) -> _TypePreservingView:
175
+ """Drop values using Python slice-start semantics.
176
+
177
+ Negative counts behave like ``list(iterable)[count:]`` and therefore
178
+ require complete input materialization.
179
+ """
180
+ return cast(_TypePreservingView, Drop[object](count))
181
+
182
+
183
+ def dropwhile[InputT](predicate: Callable[[InputT], bool]) -> DropWhile[InputT]:
184
+ """Drop initial values while ``predicate`` remains ``True``."""
185
+ return DropWhile(predicate)
186
+
187
+
188
+ def drop_while[InputT](predicate: Callable[[InputT], bool]) -> DropWhile[InputT]:
189
+ """Alias for :func:`dropwhile` using C++-style word separation."""
190
+ return dropwhile(predicate)
191
+
192
+
193
+ def counted(count: int) -> _TypePreservingView:
194
+ """Consume at most ``count`` values from the current iterator position.
195
+
196
+ Unlike ``take``, ``counted`` does not first materialize the entire input.
197
+ ``count`` must be non-negative.
198
+ """
199
+ return cast(_TypePreservingView, Counted[object](count))
200
+
201
+
202
+ def elements(index: int) -> Elements[Any]:
203
+ """Project integer-indexed field ``index`` from every input value."""
204
+ return Elements[Any](index)
205
+
206
+
207
+ def keys() -> _KeysView:
208
+ """Project field ``0`` from every tuple-like input value."""
209
+ return cast(_KeysView, Keys[Any]())
210
+
211
+
212
+ def values() -> _ValuesView:
213
+ """Project field ``1`` from every tuple-like input value."""
214
+ return cast(_ValuesView, Values[Any]())
215
+
216
+
217
+ def enumerate(start: int = 0) -> _EnumerateView:
218
+ """Pair each input value with a sequential integer index."""
219
+ return cast(_EnumerateView, Enumerate[object](start))
220
+
221
+
222
+ def concat[InputT](*iterables: Iterable[InputT]) -> Concat[InputT]:
223
+ """Append configured iterables after the pipeline input."""
224
+ return Concat(*iterables)
225
+
226
+
227
+ def zip[OtherT](*iterables: Iterable[OtherT]) -> _ZipView[OtherT]:
228
+ """Zip the pipeline input with configured iterables to the shortest length."""
229
+ return cast(_ZipView[OtherT], Zip[Any](*iterables))
230
+
231
+
232
+ def zip_transform[OutputT](
233
+ func: Callable[..., OutputT], *iterables: Iterable[Any]
234
+ ) -> ZipTransform[OutputT]:
235
+ """Zip corresponding values and call ``func`` for each group."""
236
+ return ZipTransform(func, *iterables)
237
+
238
+
239
+ def adjacent(width: int = 2) -> _AdjacentView:
240
+ """Return overlapping tuple windows of ``width`` values."""
241
+ return cast(_AdjacentView, Adjacent[object](width))
242
+
243
+
244
+ def pairwise() -> _PairwiseView:
245
+ """Return overlapping two-value tuples."""
246
+ return cast(_PairwiseView, Pairwise[object]())
247
+
248
+
249
+ def adjacent_transform[OutputT](
250
+ func: Callable[..., OutputT], width: int = 2
251
+ ) -> AdjacentTransform[Any, OutputT]:
252
+ """Call ``func`` for each overlapping window of ``width`` values."""
253
+ return AdjacentTransform[Any, OutputT](func, width)
254
+
255
+
256
+ def pairwise_transform[InputT, OutputT](
257
+ func: Callable[[InputT, InputT], OutputT],
258
+ ) -> PairwiseTransform[InputT, OutputT]:
259
+ """Call a binary ``func`` for each adjacent pair."""
260
+ return PairwiseTransform(func)
261
+
262
+
263
+ def chunk(size: int) -> _ChunkView:
264
+ """Partition the input into non-overlapping ``Range`` chunks.
265
+
266
+ ``size`` must be positive. The final chunk may contain fewer values.
267
+ """
268
+ return cast(_ChunkView, Chunk[object](size))
269
+
270
+
271
+ def slide(width: int) -> _ChunkView:
272
+ """Return overlapping ``Range`` windows of exactly ``width`` values."""
273
+ return cast(_ChunkView, Slide[object](width))
274
+
275
+
276
+ def chunk_by[InputT](
277
+ predicate: Callable[[InputT, InputT], bool],
278
+ ) -> ChunkBy[InputT]:
279
+ """Split whenever ``predicate(previous, current)`` returns ``False``."""
280
+ return ChunkBy(predicate)
281
+
282
+
283
+ def stride(step: int) -> _TypePreservingView:
284
+ """Select every ``step``-th value, starting with the first.
285
+
286
+ ``step`` must be positive.
287
+ """
288
+ return cast(_TypePreservingView, Stride[object](step))
289
+
290
+
291
+ def cartesian_product[InputT](
292
+ *iterables: Iterable[InputT],
293
+ ) -> _CartesianProductView[InputT]:
294
+ """Return the Cartesian product of the input and configured iterables."""
295
+ return cast(_CartesianProductView[InputT], CartesianProduct[Any](*iterables))
296
+
297
+
298
+ def join() -> _JoinView:
299
+ """Flatten one level of nested iterables."""
300
+ return cast(_JoinView, Join[object]())
301
+
302
+
303
+ def join_with[InputT](separator: InputT | Iterable[InputT]) -> JoinWith[InputT]:
304
+ """Flatten nested iterables with ``separator`` inserted between them."""
305
+ return JoinWith(separator)
306
+
307
+
308
+ def split[InputT](separator: InputT | Iterable[InputT]) -> Split[InputT]:
309
+ """Split input wherever the separator pattern occurs.
310
+
311
+ Empty chunks are preserved. An empty separator raises ``ValueError`` when
312
+ the adaptor is applied.
313
+ """
314
+ return Split(separator)
315
+
316
+
317
+ __all__ = [
318
+ "adjacent",
319
+ "adjacent_transform",
320
+ "all",
321
+ "cartesian_product",
322
+ "chunk",
323
+ "chunk_by",
324
+ "concat",
325
+ "counted",
326
+ "drop",
327
+ "drop_while",
328
+ "dropwhile",
329
+ "elements",
330
+ "enumerate",
331
+ "filter",
332
+ "join",
333
+ "join_with",
334
+ "keys",
335
+ "pairwise",
336
+ "pairwise_transform",
337
+ "reverse",
338
+ "slide",
339
+ "split",
340
+ "stride",
341
+ "take",
342
+ "take_while",
343
+ "takewhile",
344
+ "to",
345
+ "transform",
346
+ "values",
347
+ "zip",
348
+ "zip_transform",
349
+ ]
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: rangeslib
3
+ Version: 0.6.0
4
+ Summary: Composable, typed Python ranges and iterable adaptors
5
+ Author: Aaryan Banerjee
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TheUltimateOrion/RangesLib
8
+ Project-URL: Documentation, https://theultimateorion.github.io/RangesLib/
9
+ Project-URL: Repository, https://github.com/TheUltimateOrion/RangesLib.git
10
+ Project-URL: Issues, https://github.com/TheUltimateOrion/RangesLib/issues
11
+ Keywords: ranges,iterables,pipelines,functional,typing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: docs
23
+ Requires-Dist: myst-parser>=4; extra == "docs"
24
+ Requires-Dist: sphinx>=8; extra == "docs"
25
+ Requires-Dist: sphinx-autodoc-typehints>=3; extra == "docs"
26
+ Provides-Extra: test
27
+ Requires-Dist: coverage[toml]>=7.6; extra == "test"
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2; extra == "dev"
30
+ Requires-Dist: coverage[toml]>=7.6; extra == "dev"
31
+ Requires-Dist: mypy>=1.15; extra == "dev"
32
+ Requires-Dist: myst-parser>=4; extra == "dev"
33
+ Requires-Dist: pre-commit>=4.0; extra == "dev"
34
+ Requires-Dist: pyright>=1.1.400; extra == "dev"
35
+ Requires-Dist: ruff>=0.9; extra == "dev"
36
+ Requires-Dist: sphinx>=8; extra == "dev"
37
+ Requires-Dist: sphinx-autodoc-typehints>=3; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # rangeslib
41
+
42
+ [![Tests and quality](https://github.com/TheUltimateOrion/RangesLib/actions/workflows/tests.yml/badge.svg)](https://github.com/TheUltimateOrion/RangesLib/actions/workflows/tests.yml)
43
+ [![Documentation](https://github.com/TheUltimateOrion/RangesLib/actions/workflows/docs.yml/badge.svg)](https://theultimateorion.github.io/RangesLib/)
44
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
46
+
47
+ `rangeslib` is a typed Python library for eager, C++-inspired range pipelines.
48
+ The public API is intentionally small:
49
+
50
+ ```python
51
+ from rangeslib import ranges, views
52
+ ```
53
+
54
+ The mental model is:
55
+
56
+ ```text
57
+ ranges creates values
58
+ views transforms values
59
+ views.to converts the final result
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ from rangeslib import ranges, views
66
+
67
+ result = (
68
+ ranges.iota(1, 11)
69
+ | views.filter(lambda value: value % 2 == 0)
70
+ | views.transform(lambda value: value * 10)
71
+ | views.take(3)
72
+ | views.to(list)
73
+ )
74
+
75
+ assert result == [20, 40, 60]
76
+ ```
77
+
78
+ `Range` values are eager and reusable. Most adaptors return another `Range`; the
79
+ terminal `views.to(...)` adaptor returns whatever collection or factory you ask
80
+ for.
81
+
82
+ ## Existing Iterables
83
+
84
+ Ordinary Python iterables can start pipelines too:
85
+
86
+ ```python
87
+ from rangeslib import views
88
+
89
+ text = "abcdef" | views.take(3) | views.to("".join)
90
+ assert text == "abc"
91
+
92
+ chars = "abc" | views.all()
93
+ assert list(chars) == ["a", "b", "c"]
94
+ ```
95
+
96
+ `views.all()` is the eager Python counterpart to C++ `views::all`: it adapts an
97
+ existing iterable into a reusable `Range`.
98
+
99
+ ## Reusable Pipelines
100
+
101
+ Adaptors can be composed before data is supplied:
102
+
103
+ ```python
104
+ from rangeslib import views
105
+
106
+ first_three_even = views.filter(lambda value: value % 2 == 0) | views.take(3)
107
+
108
+ assert list([1, 2, 3, 4, 5, 6] | first_three_even) == [2, 4, 6]
109
+ assert list([10, 11, 12, 14] | first_three_even) == [10, 12, 14]
110
+ ```
111
+
112
+ ## Sources And Views
113
+
114
+ The `ranges` facade creates source ranges:
115
+
116
+ ```python
117
+ from rangeslib import ranges
118
+
119
+ ranges.empty()
120
+ ranges.single("value")
121
+ ranges.iota(1, 5)
122
+ ranges.indices(3)
123
+ ranges.repeat("x", 3)
124
+ ```
125
+
126
+ The `views` facade contains transformations such as:
127
+
128
+ ```text
129
+ all, reverse, filter, transform, take, drop, counted,
130
+ elements, keys, values, enumerate, concat, zip, cartesian_product,
131
+ adjacent, pairwise, chunk, slide, stride, join, split, to
132
+ ```
133
+
134
+ See [docs/usage.md](docs/usage.md) for the full API catalog.
135
+
136
+ ## C++ Ranges Correspondence
137
+
138
+ `rangeslib` borrows naming and broad behavior from C++20/23/26 ranges, but it
139
+ is not a lazy C++ view implementation. The most important differences are:
140
+
141
+ - Python iterables replace C++ iterator/sentinel pairs.
142
+ - `Range` stores eager values instead of reference-like lazy views.
143
+ - Tuple results are ordinary Python tuples, not tuples of references.
144
+ - Python type checking is useful but cannot express every C++ tuple-like rule.
145
+
146
+ See [docs/cpp-comparison.md](docs/cpp-comparison.md) for details.
147
+
148
+ ## Installation
149
+
150
+ Python 3.12 or newer is required.
151
+
152
+ ```bash
153
+ python -m pip install .
154
+ ```
155
+
156
+ For development:
157
+
158
+ ```bash
159
+ python -m pip install -e ".[dev]"
160
+ ```
161
+
162
+ `rangeslib` has no runtime dependencies.
163
+
164
+ ## Development
165
+
166
+ Useful commands live in `scripts/`:
167
+
168
+ ```bash
169
+ ./scripts/run_tests.sh # tests only
170
+ ./scripts/typecheck.sh # mypy + Pyright
171
+ ./scripts/check.sh # Ruff, typing, tests, coverage
172
+ ./scripts/check_all.sh # check.sh + strict Sphinx docs
173
+ ./scripts/check_package.sh # sdist/wheel build and install smoke test
174
+ ./scripts/run_playground.sh # manual playground
175
+ ```
176
+
177
+ Before a release commit, run:
178
+
179
+ ```bash
180
+ ./scripts/check_all.sh
181
+ ./scripts/check_package.sh
182
+ ```
183
+
184
+ ## Documentation
185
+
186
+ Build the Sphinx site with:
187
+
188
+ ```bash
189
+ ./scripts/generate_docs.sh
190
+ ```
191
+
192
+ Generated HTML is written to `docs/_build/html/` and published to GitHub Pages
193
+ after CI succeeds on `main`.
194
+
195
+ ## Releases
196
+
197
+ Changing `[project].version` in `pyproject.toml` on `main` automatically runs
198
+ the complete quality and package checks, then creates the matching Git tag and
199
+ GitHub Release. Published GitHub Releases trigger PyPI publishing through
200
+ Trusted Publishing after the PyPI project is configured.
201
+
202
+ For future work, prefer opening or collecting issues before adding more adaptors
203
+ immediately. See [docs/roadmap.md](docs/roadmap.md).
204
+
205
+ See [docs/publishing.md](docs/publishing.md) for the PyPI setup checklist.
@@ -0,0 +1,12 @@
1
+ rangeslib/__init__.py,sha256=ICZfHAQmeueQKKJZfPNOd5-4uQIu7qMCkQ3-fpv_VAI,108
2
+ rangeslib/_adaptors.py,sha256=sxUuvJ7VbOFUTM1ZCu7e_e6HAaneYs-Y4VBOjuZT-Ic,13448
3
+ rangeslib/_core.py,sha256=3abc6w6HSwfzModY2hsnIPF2BedZM6Oob8OSDB5Zpng,3198
4
+ rangeslib/_generators.py,sha256=fP26T0i_r-xCUSGFNtwh7ew4wtz6d4HWuW8lxOlzOyM,1127
5
+ rangeslib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ rangeslib/ranges.py,sha256=qfC7fJ-UbMIeWNvitSltFqW_zW-WVr7L7vu1K3_NsaI,826
7
+ rangeslib/views.py,sha256=INxSEanBhxX1g3teKE2tp9gbhUJxRpbnoyzBgAkAl-Y,9798
8
+ rangeslib-0.6.0.dist-info/licenses/LICENSE,sha256=UAvw4k0gMkbpVv5MiD7icihSYrcoy-Zsb-CDtbc1MsE,1072
9
+ rangeslib-0.6.0.dist-info/METADATA,sha256=tV7x5pw0sEQI-wflTMcCBF39iFTk2Z_3SEKBiD9iGHE,6013
10
+ rangeslib-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ rangeslib-0.6.0.dist-info/top_level.txt,sha256=4BNUUUJHbKIq--KOxgHOksvjcI6Ph_0sRM0oE6S06aQ,10
12
+ rangeslib-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaryan Banerjee
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, sublicense, 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.
@@ -0,0 +1 @@
1
+ rangeslib