countably 2026.5.16.56074.dev1__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.
- countably/__init__.py +7 -0
- countably/_core.py +294 -0
- countably/tests/__init__.py +0 -0
- countably/tests/test_arithmetic.py +153 -0
- countably/tests/test_coercion.py +33 -0
- countably/tests/test_primitives.py +87 -0
- countably/tests/test_slicing.py +140 -0
- countably/tests/test_version.py +9 -0
- countably-2026.5.16.56074.dev1.dist-info/METADATA +41 -0
- countably-2026.5.16.56074.dev1.dist-info/RECORD +13 -0
- countably-2026.5.16.56074.dev1.dist-info/WHEEL +5 -0
- countably-2026.5.16.56074.dev1.dist-info/entry_points.txt +2 -0
- countably-2026.5.16.56074.dev1.dist-info/top_level.txt +1 -0
countably/__init__.py
ADDED
countably/_core.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
import operator
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import (
|
|
9
|
+
Callable,
|
|
10
|
+
Generic,
|
|
11
|
+
Iterator,
|
|
12
|
+
Protocol,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
overload,
|
|
16
|
+
runtime_checkable,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Number = complex
|
|
20
|
+
SeqOrNumber = Union["NumberSequence[Number]", Number]
|
|
21
|
+
|
|
22
|
+
T_co = TypeVar("T_co", covariant=True, bound=Number)
|
|
23
|
+
T = TypeVar("T", bound=Number)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Computation(Protocol[T_co]):
|
|
27
|
+
def __len__(self) -> int: ...
|
|
28
|
+
|
|
29
|
+
def __getitem__(self, index: int) -> T_co: ...
|
|
30
|
+
|
|
31
|
+
def __iter__(self) -> Iterator[T_co]: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@runtime_checkable
|
|
35
|
+
class NumberSequence(Protocol[T_co]):
|
|
36
|
+
def __len__(self) -> int: ...
|
|
37
|
+
|
|
38
|
+
def __bool__(self) -> bool: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def __getitem__(self, index: int) -> T_co: ...
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def __getitem__(self, index: slice) -> "NumberSequence[T_co]": ...
|
|
45
|
+
|
|
46
|
+
def __iter__(self) -> Iterator[T_co]: ...
|
|
47
|
+
|
|
48
|
+
def __add__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
49
|
+
|
|
50
|
+
def __radd__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
51
|
+
|
|
52
|
+
def __sub__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
53
|
+
|
|
54
|
+
def __rsub__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
55
|
+
|
|
56
|
+
def __mul__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
57
|
+
|
|
58
|
+
def __rmul__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
59
|
+
|
|
60
|
+
def __truediv__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
61
|
+
|
|
62
|
+
def __rtruediv__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
63
|
+
|
|
64
|
+
def __floordiv__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
65
|
+
|
|
66
|
+
def __rfloordiv__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
67
|
+
|
|
68
|
+
def __mod__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
69
|
+
|
|
70
|
+
def __rmod__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
71
|
+
|
|
72
|
+
def __pow__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
73
|
+
|
|
74
|
+
def __rpow__(self, other: SeqOrNumber) -> "NumberSequence[Number]": ...
|
|
75
|
+
|
|
76
|
+
def __neg__(self) -> "NumberSequence[Number]": ...
|
|
77
|
+
|
|
78
|
+
def __pos__(self) -> "NumberSequence[Number]": ...
|
|
79
|
+
|
|
80
|
+
def __abs__(self) -> "NumberSequence[Number]": ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, kw_only=True)
|
|
84
|
+
class _Sequence(Generic[T_co]):
|
|
85
|
+
_computation: _Computation[T_co]
|
|
86
|
+
|
|
87
|
+
@functools.cached_property
|
|
88
|
+
def _cached_at(self) -> Callable[[int], T_co]:
|
|
89
|
+
return functools.lru_cache(maxsize=100)(self._computation.__getitem__)
|
|
90
|
+
|
|
91
|
+
def __len__(self) -> int:
|
|
92
|
+
return len(self._computation)
|
|
93
|
+
|
|
94
|
+
def __bool__(self) -> bool:
|
|
95
|
+
raise TypeError("NumberSequence has no boolean value")
|
|
96
|
+
|
|
97
|
+
@overload
|
|
98
|
+
def __getitem__(self, index: int) -> T_co: ...
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
def __getitem__(self, index: slice) -> "_Sequence[T_co]": ...
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, index: Union[int, slice]) -> Union[T_co, "_Sequence[T_co]"]:
|
|
104
|
+
if isinstance(index, slice):
|
|
105
|
+
return _slice_sequence(self, index)
|
|
106
|
+
size = len(self)
|
|
107
|
+
position = index
|
|
108
|
+
if position < 0:
|
|
109
|
+
if size == sys.maxsize:
|
|
110
|
+
raise IndexError("negative index on infinite sequence")
|
|
111
|
+
position += size
|
|
112
|
+
if position < 0 or position >= size:
|
|
113
|
+
raise IndexError(index)
|
|
114
|
+
return self._cached_at(position)
|
|
115
|
+
|
|
116
|
+
def __iter__(self) -> Iterator[T_co]:
|
|
117
|
+
return iter(self._computation)
|
|
118
|
+
|
|
119
|
+
def __add__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
120
|
+
return _binop(self, other, operator.add)
|
|
121
|
+
|
|
122
|
+
def __radd__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
123
|
+
return _binop(other, self, operator.add)
|
|
124
|
+
|
|
125
|
+
def __sub__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
126
|
+
return _binop(self, other, operator.sub)
|
|
127
|
+
|
|
128
|
+
def __rsub__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
129
|
+
return _binop(other, self, operator.sub)
|
|
130
|
+
|
|
131
|
+
def __mul__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
132
|
+
return _binop(self, other, operator.mul)
|
|
133
|
+
|
|
134
|
+
def __rmul__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
135
|
+
return _binop(other, self, operator.mul)
|
|
136
|
+
|
|
137
|
+
def __truediv__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
138
|
+
return _binop(self, other, operator.truediv)
|
|
139
|
+
|
|
140
|
+
def __rtruediv__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
141
|
+
return _binop(other, self, operator.truediv)
|
|
142
|
+
|
|
143
|
+
def __floordiv__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
144
|
+
return _binop(self, other, operator.floordiv)
|
|
145
|
+
|
|
146
|
+
def __rfloordiv__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
147
|
+
return _binop(other, self, operator.floordiv)
|
|
148
|
+
|
|
149
|
+
def __mod__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
150
|
+
return _binop(self, other, operator.mod)
|
|
151
|
+
|
|
152
|
+
def __rmod__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
153
|
+
return _binop(other, self, operator.mod)
|
|
154
|
+
|
|
155
|
+
def __pow__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
156
|
+
return _binop(self, other, operator.pow)
|
|
157
|
+
|
|
158
|
+
def __rpow__(self, other: SeqOrNumber) -> NumberSequence[Number]:
|
|
159
|
+
return _binop(other, self, operator.pow)
|
|
160
|
+
|
|
161
|
+
def __neg__(self) -> NumberSequence[Number]:
|
|
162
|
+
return _unop(self, operator.neg)
|
|
163
|
+
|
|
164
|
+
def __pos__(self) -> NumberSequence[Number]:
|
|
165
|
+
return _unop(self, operator.pos)
|
|
166
|
+
|
|
167
|
+
def __abs__(self) -> NumberSequence[Number]:
|
|
168
|
+
return _unop(self, operator.abs)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
172
|
+
class _ConstantComputation(Generic[T]):
|
|
173
|
+
value: T
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
return sys.maxsize
|
|
177
|
+
|
|
178
|
+
def __getitem__(self, index: int) -> T:
|
|
179
|
+
return self.value
|
|
180
|
+
|
|
181
|
+
def __iter__(self) -> Iterator[T]:
|
|
182
|
+
return itertools.repeat(self.value)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
186
|
+
class _CountComputation:
|
|
187
|
+
def __len__(self) -> int:
|
|
188
|
+
return sys.maxsize
|
|
189
|
+
|
|
190
|
+
def __getitem__(self, index: int) -> int:
|
|
191
|
+
return index
|
|
192
|
+
|
|
193
|
+
def __iter__(self) -> Iterator[int]:
|
|
194
|
+
return itertools.count()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
198
|
+
class _BinOpComputation:
|
|
199
|
+
left: NumberSequence[Number]
|
|
200
|
+
right: NumberSequence[Number]
|
|
201
|
+
op: Callable[[Number, Number], Number]
|
|
202
|
+
|
|
203
|
+
def __len__(self) -> int:
|
|
204
|
+
return min(len(self.left), len(self.right))
|
|
205
|
+
|
|
206
|
+
def __getitem__(self, index: int) -> Number:
|
|
207
|
+
return self.op(self.left[index], self.right[index])
|
|
208
|
+
|
|
209
|
+
def __iter__(self) -> Iterator[Number]:
|
|
210
|
+
return map(self.op, self.left, self.right)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
214
|
+
class _UnaryOpComputation:
|
|
215
|
+
seq: NumberSequence[Number]
|
|
216
|
+
op: Callable[[Number], Number]
|
|
217
|
+
|
|
218
|
+
def __len__(self) -> int:
|
|
219
|
+
return len(self.seq)
|
|
220
|
+
|
|
221
|
+
def __getitem__(self, index: int) -> Number:
|
|
222
|
+
return self.op(self.seq[index])
|
|
223
|
+
|
|
224
|
+
def __iter__(self) -> Iterator[Number]:
|
|
225
|
+
return map(self.op, self.seq)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
229
|
+
class _SlicedComputation(Generic[T_co]):
|
|
230
|
+
source: NumberSequence[T_co]
|
|
231
|
+
start: int
|
|
232
|
+
step: int
|
|
233
|
+
length: int
|
|
234
|
+
|
|
235
|
+
def __len__(self) -> int:
|
|
236
|
+
return self.length
|
|
237
|
+
|
|
238
|
+
def __getitem__(self, index: int) -> T_co:
|
|
239
|
+
return self.source[self.start + self.step * index]
|
|
240
|
+
|
|
241
|
+
def __iter__(self) -> Iterator[T_co]:
|
|
242
|
+
stop = (
|
|
243
|
+
None if self.length == sys.maxsize else self.start + self.step * self.length
|
|
244
|
+
)
|
|
245
|
+
return itertools.islice(self.source, self.start, stop, self.step)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _coerce(value: SeqOrNumber) -> NumberSequence[Number]:
|
|
249
|
+
if isinstance(value, NumberSequence):
|
|
250
|
+
return value
|
|
251
|
+
return constant(value)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _binop(
|
|
255
|
+
left: SeqOrNumber,
|
|
256
|
+
right: SeqOrNumber,
|
|
257
|
+
op: Callable[[Number, Number], Number],
|
|
258
|
+
) -> NumberSequence[Number]:
|
|
259
|
+
return _Sequence(
|
|
260
|
+
_computation=_BinOpComputation(left=_coerce(left), right=_coerce(right), op=op)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _unop(
|
|
265
|
+
seq: NumberSequence[Number],
|
|
266
|
+
op: Callable[[Number], Number],
|
|
267
|
+
) -> NumberSequence[Number]:
|
|
268
|
+
return _Sequence(_computation=_UnaryOpComputation(seq=seq, op=op))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _slice_sequence(seq: NumberSequence[T_co], sl: slice) -> _Sequence[T_co]:
|
|
272
|
+
step = 1 if sl.step is None else sl.step
|
|
273
|
+
start = 0 if sl.start is None else sl.start
|
|
274
|
+
if step <= 0 or start < 0 or (sl.stop is not None and sl.stop < 0):
|
|
275
|
+
raise ValueError(f"invalid slice: {sl!r}")
|
|
276
|
+
source_len = len(seq)
|
|
277
|
+
if sl.stop is None and source_len == sys.maxsize:
|
|
278
|
+
length = sys.maxsize
|
|
279
|
+
else:
|
|
280
|
+
actual_stop = source_len if sl.stop is None else min(sl.stop, source_len)
|
|
281
|
+
length = max(0, (actual_stop - start + step - 1) // step)
|
|
282
|
+
return _Sequence(
|
|
283
|
+
_computation=_SlicedComputation(
|
|
284
|
+
source=seq, start=start, step=step, length=length
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def constant(value: T) -> NumberSequence[T]:
|
|
290
|
+
return _Sequence(_computation=_ConstantComputation(value=value))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def count() -> NumberSequence[int]:
|
|
294
|
+
return _Sequence(_computation=_CountComputation())
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from hamcrest import assert_that, calling, equal_to, raises
|
|
6
|
+
|
|
7
|
+
from countably import constant, count
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _take(seq, n):
|
|
11
|
+
return list(itertools.islice(seq, n))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestAdd(unittest.TestCase):
|
|
15
|
+
def test_two_constants(self) -> None:
|
|
16
|
+
seq = constant(3) + constant(5)
|
|
17
|
+
assert_that(seq[0], equal_to(8))
|
|
18
|
+
assert_that(seq[100], equal_to(8))
|
|
19
|
+
|
|
20
|
+
def test_constant_plus_count(self) -> None:
|
|
21
|
+
seq = constant(3) + count()
|
|
22
|
+
assert_that(_take(seq, 4), equal_to([3, 4, 5, 6]))
|
|
23
|
+
|
|
24
|
+
def test_left_coerce(self) -> None:
|
|
25
|
+
seq = 3 + count()
|
|
26
|
+
assert_that(_take(seq, 4), equal_to([3, 4, 5, 6]))
|
|
27
|
+
|
|
28
|
+
def test_right_coerce(self) -> None:
|
|
29
|
+
seq = count() + 3
|
|
30
|
+
assert_that(_take(seq, 4), equal_to([3, 4, 5, 6]))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestSub(unittest.TestCase):
|
|
34
|
+
def test_count_minus_one(self) -> None:
|
|
35
|
+
seq = count() - 1
|
|
36
|
+
assert_that(_take(seq, 4), equal_to([-1, 0, 1, 2]))
|
|
37
|
+
|
|
38
|
+
def test_left_coerce(self) -> None:
|
|
39
|
+
seq = 10 - count()
|
|
40
|
+
assert_that(_take(seq, 4), equal_to([10, 9, 8, 7]))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestMul(unittest.TestCase):
|
|
44
|
+
def test_count_times_two(self) -> None:
|
|
45
|
+
seq = count() * 2
|
|
46
|
+
assert_that(_take(seq, 4), equal_to([0, 2, 4, 6]))
|
|
47
|
+
|
|
48
|
+
def test_left_coerce(self) -> None:
|
|
49
|
+
seq = 5 * count()
|
|
50
|
+
assert_that(_take(seq, 4), equal_to([0, 5, 10, 15]))
|
|
51
|
+
|
|
52
|
+
def test_complex_expression(self) -> None:
|
|
53
|
+
seq = 3 + 5 * count()
|
|
54
|
+
assert_that(_take(seq, 4), equal_to([3, 8, 13, 18]))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestDiv(unittest.TestCase):
|
|
58
|
+
def test_truediv(self) -> None:
|
|
59
|
+
seq = (count() + 1) / 2
|
|
60
|
+
assert_that(_take(seq, 4), equal_to([0.5, 1.0, 1.5, 2.0]))
|
|
61
|
+
|
|
62
|
+
def test_truediv_left_coerce(self) -> None:
|
|
63
|
+
seq = 10 / (count() + 1)
|
|
64
|
+
assert_that(_take(seq, 4), equal_to([10.0, 5.0, 10 / 3, 2.5]))
|
|
65
|
+
|
|
66
|
+
def test_floordiv(self) -> None:
|
|
67
|
+
seq = count() // 2
|
|
68
|
+
assert_that(_take(seq, 5), equal_to([0, 0, 1, 1, 2]))
|
|
69
|
+
|
|
70
|
+
def test_floordiv_left_coerce(self) -> None:
|
|
71
|
+
seq = 10 // (count() + 1)
|
|
72
|
+
assert_that(_take(seq, 4), equal_to([10, 5, 3, 2]))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestMod(unittest.TestCase):
|
|
76
|
+
def test_mod(self) -> None:
|
|
77
|
+
seq = count() % 3
|
|
78
|
+
assert_that(_take(seq, 7), equal_to([0, 1, 2, 0, 1, 2, 0]))
|
|
79
|
+
|
|
80
|
+
def test_mod_left_coerce(self) -> None:
|
|
81
|
+
seq = 10 % (count() + 1)
|
|
82
|
+
assert_that(_take(seq, 5), equal_to([0, 0, 1, 2, 0]))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestPow(unittest.TestCase):
|
|
86
|
+
def test_pow(self) -> None:
|
|
87
|
+
seq = count() ** 2
|
|
88
|
+
assert_that(_take(seq, 5), equal_to([0, 1, 4, 9, 16]))
|
|
89
|
+
|
|
90
|
+
def test_pow_left_coerce(self) -> None:
|
|
91
|
+
seq = 2 ** count()
|
|
92
|
+
assert_that(_take(seq, 5), equal_to([1, 2, 4, 8, 16]))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestUnary(unittest.TestCase):
|
|
96
|
+
def test_neg(self) -> None:
|
|
97
|
+
seq = -count()
|
|
98
|
+
assert_that(_take(seq, 4), equal_to([0, -1, -2, -3]))
|
|
99
|
+
|
|
100
|
+
def test_pos(self) -> None:
|
|
101
|
+
seq = +(count() - 2)
|
|
102
|
+
assert_that(_take(seq, 4), equal_to([-2, -1, 0, 1]))
|
|
103
|
+
|
|
104
|
+
def test_abs(self) -> None:
|
|
105
|
+
seq = abs(count() - 2)
|
|
106
|
+
assert_that(_take(seq, 5), equal_to([2, 1, 0, 1, 2]))
|
|
107
|
+
|
|
108
|
+
def test_unary_length_preserved(self) -> None:
|
|
109
|
+
seq = -count()[2:10]
|
|
110
|
+
assert_that(len(seq), equal_to(8))
|
|
111
|
+
|
|
112
|
+
def test_neg_index(self) -> None:
|
|
113
|
+
seq = -count()
|
|
114
|
+
assert_that(seq[5], equal_to(-5))
|
|
115
|
+
|
|
116
|
+
def test_abs_index(self) -> None:
|
|
117
|
+
seq = abs(count() - 3)
|
|
118
|
+
assert_that(seq[0], equal_to(3))
|
|
119
|
+
assert_that(seq[5], equal_to(2))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestLength(unittest.TestCase):
|
|
123
|
+
def test_binop_infinite(self) -> None:
|
|
124
|
+
seq = constant(3) + count()
|
|
125
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
126
|
+
|
|
127
|
+
def test_binop_finite_takes_shorter(self) -> None:
|
|
128
|
+
seq = count()[0:5] + count()[0:3]
|
|
129
|
+
assert_that(len(seq), equal_to(3))
|
|
130
|
+
|
|
131
|
+
def test_binop_finite_with_infinite(self) -> None:
|
|
132
|
+
seq = count()[0:5] + count()
|
|
133
|
+
assert_that(len(seq), equal_to(5))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestImmutability(unittest.TestCase):
|
|
137
|
+
def test_sequence_is_frozen(self) -> None:
|
|
138
|
+
from dataclasses import FrozenInstanceError
|
|
139
|
+
|
|
140
|
+
seq = constant(7)
|
|
141
|
+
assert_that(
|
|
142
|
+
calling(setattr).with_args(seq, "_computation", None),
|
|
143
|
+
raises(FrozenInstanceError),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def test_equality_of_expressions(self) -> None:
|
|
147
|
+
first = 3 + count()
|
|
148
|
+
second = 3 + count()
|
|
149
|
+
assert_that(first, equal_to(second))
|
|
150
|
+
|
|
151
|
+
def test_hashable(self) -> None:
|
|
152
|
+
seq = 3 + count()
|
|
153
|
+
assert_that(hash(seq), equal_to(hash(3 + count())))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from hamcrest import assert_that, equal_to
|
|
5
|
+
|
|
6
|
+
from countably import constant, count
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _take(seq, n):
|
|
10
|
+
return list(itertools.islice(seq, n))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestCoercion(unittest.TestCase):
|
|
14
|
+
def test_int_on_left_matches_explicit_constant(self) -> None:
|
|
15
|
+
assert_that(3 + count(), equal_to(constant(3) + count()))
|
|
16
|
+
|
|
17
|
+
def test_int_on_right_matches_explicit_constant(self) -> None:
|
|
18
|
+
assert_that(count() + 3, equal_to(count() + constant(3)))
|
|
19
|
+
|
|
20
|
+
def test_float_coerces(self) -> None:
|
|
21
|
+
seq = 0.5 + count()
|
|
22
|
+
assert_that(_take(seq, 3), equal_to([0.5, 1.5, 2.5]))
|
|
23
|
+
|
|
24
|
+
def test_complex_coerces(self) -> None:
|
|
25
|
+
seq = 1j + count()
|
|
26
|
+
assert_that(_take(seq, 3), equal_to([1j, 1 + 1j, 2 + 1j]))
|
|
27
|
+
|
|
28
|
+
def test_chained_coercion(self) -> None:
|
|
29
|
+
seq = 3 + 5 * count()
|
|
30
|
+
assert_that(_take(seq, 4), equal_to([3, 8, 13, 18]))
|
|
31
|
+
|
|
32
|
+
def test_coerced_value_at_index(self) -> None:
|
|
33
|
+
assert_that((10 - count())[3], equal_to(7))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import operator
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from hamcrest import assert_that, calling, equal_to, raises
|
|
7
|
+
|
|
8
|
+
from countably import NumberSequence, constant, count
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestConstant(unittest.TestCase):
|
|
12
|
+
def test_value_at_zero(self) -> None:
|
|
13
|
+
seq = constant(7)
|
|
14
|
+
assert_that(seq[0], equal_to(7))
|
|
15
|
+
|
|
16
|
+
def test_value_at_large_index(self) -> None:
|
|
17
|
+
seq = constant(7)
|
|
18
|
+
assert_that(seq[10_000], equal_to(7))
|
|
19
|
+
|
|
20
|
+
def test_is_sequence(self) -> None:
|
|
21
|
+
assert_that(isinstance(constant(7), NumberSequence), equal_to(True))
|
|
22
|
+
|
|
23
|
+
def test_length_is_maxsize(self) -> None:
|
|
24
|
+
assert_that(len(constant(7)), equal_to(sys.maxsize))
|
|
25
|
+
|
|
26
|
+
def test_equality(self) -> None:
|
|
27
|
+
assert_that(constant(7), equal_to(constant(7)))
|
|
28
|
+
|
|
29
|
+
def test_iter(self) -> None:
|
|
30
|
+
values = list(itertools.islice(constant(7), 4))
|
|
31
|
+
assert_that(values, equal_to([7, 7, 7, 7]))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestCount(unittest.TestCase):
|
|
35
|
+
def test_value_at_zero(self) -> None:
|
|
36
|
+
assert_that(count()[0], equal_to(0))
|
|
37
|
+
|
|
38
|
+
def test_value_at_index(self) -> None:
|
|
39
|
+
assert_that(count()[42], equal_to(42))
|
|
40
|
+
|
|
41
|
+
def test_length_is_maxsize(self) -> None:
|
|
42
|
+
assert_that(len(count()), equal_to(sys.maxsize))
|
|
43
|
+
|
|
44
|
+
def test_is_sequence(self) -> None:
|
|
45
|
+
assert_that(isinstance(count(), NumberSequence), equal_to(True))
|
|
46
|
+
|
|
47
|
+
def test_equality(self) -> None:
|
|
48
|
+
assert_that(count(), equal_to(count()))
|
|
49
|
+
|
|
50
|
+
def test_iter(self) -> None:
|
|
51
|
+
values = list(itertools.islice(count(), 5))
|
|
52
|
+
assert_that(values, equal_to([0, 1, 2, 3, 4]))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestBoolFails(unittest.TestCase):
|
|
56
|
+
def test_bool_constant_raises(self) -> None:
|
|
57
|
+
assert_that(calling(bool).with_args(constant(7)), raises(TypeError))
|
|
58
|
+
|
|
59
|
+
def test_bool_count_raises(self) -> None:
|
|
60
|
+
assert_that(calling(bool).with_args(count()), raises(TypeError))
|
|
61
|
+
|
|
62
|
+
def test_bool_expression_raises(self) -> None:
|
|
63
|
+
assert_that(calling(bool).with_args(3 + count()), raises(TypeError))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestIndexBounds(unittest.TestCase):
|
|
67
|
+
def test_negative_index_on_infinite_raises(self) -> None:
|
|
68
|
+
assert_that(
|
|
69
|
+
calling(operator.getitem).with_args(count(), -1),
|
|
70
|
+
raises(IndexError),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_out_of_range_finite_raises(self) -> None:
|
|
74
|
+
assert_that(
|
|
75
|
+
calling(operator.getitem).with_args(count()[0:3], 3),
|
|
76
|
+
raises(IndexError),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def test_finite_negative_index_works(self) -> None:
|
|
80
|
+
seq = count()[2:10]
|
|
81
|
+
assert_that(seq[-1], equal_to(9))
|
|
82
|
+
|
|
83
|
+
def test_finite_negative_out_of_range_raises(self) -> None:
|
|
84
|
+
assert_that(
|
|
85
|
+
calling(operator.getitem).with_args(count()[2:5], -4),
|
|
86
|
+
raises(IndexError),
|
|
87
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import operator
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from hamcrest import assert_that, calling, equal_to, raises
|
|
7
|
+
|
|
8
|
+
from countably import constant, count
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _take(seq, n):
|
|
12
|
+
return list(itertools.islice(seq, n))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestInfiniteSlice(unittest.TestCase):
|
|
16
|
+
def test_default_slice(self) -> None:
|
|
17
|
+
seq = count()[:]
|
|
18
|
+
assert_that(_take(seq, 4), equal_to([0, 1, 2, 3]))
|
|
19
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
20
|
+
|
|
21
|
+
def test_start_only(self) -> None:
|
|
22
|
+
seq = count()[3:]
|
|
23
|
+
assert_that(_take(seq, 4), equal_to([3, 4, 5, 6]))
|
|
24
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
25
|
+
|
|
26
|
+
def test_step_only(self) -> None:
|
|
27
|
+
seq = count()[::2]
|
|
28
|
+
assert_that(_take(seq, 4), equal_to([0, 2, 4, 6]))
|
|
29
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
30
|
+
|
|
31
|
+
def test_start_and_step(self) -> None:
|
|
32
|
+
seq = count()[2::3]
|
|
33
|
+
assert_that(_take(seq, 4), equal_to([2, 5, 8, 11]))
|
|
34
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestFiniteSlice(unittest.TestCase):
|
|
38
|
+
def test_start_stop(self) -> None:
|
|
39
|
+
seq = count()[2:7]
|
|
40
|
+
assert_that(len(seq), equal_to(5))
|
|
41
|
+
assert_that(list(seq), equal_to([2, 3, 4, 5, 6]))
|
|
42
|
+
|
|
43
|
+
def test_start_stop_step(self) -> None:
|
|
44
|
+
seq = count()[2:10:3]
|
|
45
|
+
assert_that(len(seq), equal_to(3))
|
|
46
|
+
assert_that(list(seq), equal_to([2, 5, 8]))
|
|
47
|
+
|
|
48
|
+
def test_stop_smaller_than_start(self) -> None:
|
|
49
|
+
seq = count()[5:3]
|
|
50
|
+
assert_that(len(seq), equal_to(0))
|
|
51
|
+
assert_that(list(seq), equal_to([]))
|
|
52
|
+
|
|
53
|
+
def test_stop_at_boundary(self) -> None:
|
|
54
|
+
seq = count()[2:11:3]
|
|
55
|
+
assert_that(len(seq), equal_to(3))
|
|
56
|
+
assert_that(list(seq), equal_to([2, 5, 8]))
|
|
57
|
+
|
|
58
|
+
def test_stop_at_next_boundary(self) -> None:
|
|
59
|
+
seq = count()[2:12:3]
|
|
60
|
+
assert_that(len(seq), equal_to(4))
|
|
61
|
+
assert_that(list(seq), equal_to([2, 5, 8, 11]))
|
|
62
|
+
|
|
63
|
+
def test_finite_iter(self) -> None:
|
|
64
|
+
seq = count()[0:3]
|
|
65
|
+
assert_that(list(seq), equal_to([0, 1, 2]))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestSliceOfSlice(unittest.TestCase):
|
|
69
|
+
def test_finite_of_finite(self) -> None:
|
|
70
|
+
seq = count()[2:20:3][1:4]
|
|
71
|
+
assert_that(list(seq), equal_to([5, 8, 11]))
|
|
72
|
+
|
|
73
|
+
def test_infinite_of_infinite(self) -> None:
|
|
74
|
+
seq = count()[2::3][1::2]
|
|
75
|
+
assert_that(_take(seq, 4), equal_to([5, 11, 17, 23]))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestArithmeticBetweenSlices(unittest.TestCase):
|
|
79
|
+
def test_finite_plus_finite_shortest_wins(self) -> None:
|
|
80
|
+
left = count()[2:10]
|
|
81
|
+
right = count()[3:8]
|
|
82
|
+
combined = left + right
|
|
83
|
+
assert_that(len(combined), equal_to(5))
|
|
84
|
+
assert_that(list(combined), equal_to([5, 7, 9, 11, 13]))
|
|
85
|
+
|
|
86
|
+
def test_finite_plus_infinite_finite_wins(self) -> None:
|
|
87
|
+
seq = count()[0:4] + count()
|
|
88
|
+
assert_that(len(seq), equal_to(4))
|
|
89
|
+
assert_that(list(seq), equal_to([0, 2, 4, 6]))
|
|
90
|
+
|
|
91
|
+
def test_infinite_plus_infinite_infinite(self) -> None:
|
|
92
|
+
seq = count()[::2] + count()[::3]
|
|
93
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
94
|
+
assert_that(_take(seq, 4), equal_to([0, 5, 10, 15]))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestSliceErrors(unittest.TestCase):
|
|
98
|
+
def test_zero_step_raises(self) -> None:
|
|
99
|
+
assert_that(
|
|
100
|
+
calling(operator.getitem).with_args(count(), slice(0, 10, 0)),
|
|
101
|
+
raises(ValueError),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def test_negative_step_raises(self) -> None:
|
|
105
|
+
assert_that(
|
|
106
|
+
calling(operator.getitem).with_args(count(), slice(None, None, -1)),
|
|
107
|
+
raises(ValueError),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def test_negative_start_raises(self) -> None:
|
|
111
|
+
assert_that(
|
|
112
|
+
calling(operator.getitem).with_args(count(), slice(-1, None, None)),
|
|
113
|
+
raises(ValueError),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def test_negative_stop_raises(self) -> None:
|
|
117
|
+
assert_that(
|
|
118
|
+
calling(operator.getitem).with_args(count(), slice(0, -1, None)),
|
|
119
|
+
raises(ValueError),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestSliceOfConstant(unittest.TestCase):
|
|
124
|
+
def test_infinite_slice(self) -> None:
|
|
125
|
+
seq = constant(5)[2::3]
|
|
126
|
+
assert_that(_take(seq, 4), equal_to([5, 5, 5, 5]))
|
|
127
|
+
|
|
128
|
+
def test_finite_slice(self) -> None:
|
|
129
|
+
seq = constant(5)[2:5]
|
|
130
|
+
assert_that(list(seq), equal_to([5, 5, 5]))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestFiniteSliceLength(unittest.TestCase):
|
|
134
|
+
def test_finite_slice_real_len(self) -> None:
|
|
135
|
+
seq = count()[2:10]
|
|
136
|
+
assert_that(len(seq), equal_to(8))
|
|
137
|
+
|
|
138
|
+
def test_infinite_slice_maxsize_len(self) -> None:
|
|
139
|
+
seq = count()[2::3]
|
|
140
|
+
assert_that(len(seq), equal_to(sys.maxsize))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: countably
|
|
3
|
+
Version: 2026.5.16.56074.dev1
|
|
4
|
+
Summary: Infinite number sequences
|
|
5
|
+
Author-email: Moshe Zadka <moshez@zadka.club>
|
|
6
|
+
License: Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
to deal in the Software without restriction, including without limitation the
|
|
9
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is furnished
|
|
11
|
+
to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
17
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
18
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
20
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
21
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Project-URL: Homepage, https://github.com/moshez/countably
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
Provides-Extra: tests
|
|
26
|
+
Requires-Dist: virtue; extra == "tests"
|
|
27
|
+
Requires-Dist: pyhamcrest; extra == "tests"
|
|
28
|
+
Requires-Dist: coverage; extra == "tests"
|
|
29
|
+
Provides-Extra: mypy
|
|
30
|
+
Requires-Dist: mypy; extra == "mypy"
|
|
31
|
+
Provides-Extra: lint
|
|
32
|
+
Requires-Dist: flake8; extra == "lint"
|
|
33
|
+
Requires-Dist: black; extra == "lint"
|
|
34
|
+
Requires-Dist: stolid; extra == "lint"
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
37
|
+
|
|
38
|
+
countably
|
|
39
|
+
========================
|
|
40
|
+
|
|
41
|
+
Stuff
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
countably/__init__.py,sha256=kukhO1HaKvUqTEIPZ-fgpL-sx1WKUYZzxS7IP0wZLjs,196
|
|
2
|
+
countably/_core.py,sha256=Jd06DVTmevuv-Y20UJlz9UThWa4bsj0UfyetaWVkCpM,8853
|
|
3
|
+
countably/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
countably/tests/test_arithmetic.py,sha256=ZJe7jaVsD8XGZbd46SUI8f9brO4BRzenXBSa_cuTljQ,4543
|
|
5
|
+
countably/tests/test_coercion.py,sha256=1pfAu6RcP0XFKZj45U1KsTSRqgT48DBjrhsCiLGzLNk,999
|
|
6
|
+
countably/tests/test_primitives.py,sha256=GWixRWdGgSb7bKB61qXggIyBzq0RL8sHEPkjhawieus,2726
|
|
7
|
+
countably/tests/test_slicing.py,sha256=oXFCmtd8sHpTS3Vt0y8pYVA6cdqK-5vcII1q2Xi82rs,4489
|
|
8
|
+
countably/tests/test_version.py,sha256=3vebXGHF_cHyL9T9_xEgomFpYHGu7OhZCojlYBJ3SQM,232
|
|
9
|
+
countably-2026.5.16.56074.dev1.dist-info/METADATA,sha256=dacQpJ5Q_UagYj2WCn-_8pDKOgMIslLvueN4MY11ckc,1852
|
|
10
|
+
countably-2026.5.16.56074.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
countably-2026.5.16.56074.dev1.dist-info/entry_points.txt,sha256=nh1ojdfUM_YnrSmnSUIf3k_p1bb6cHF6TOYRRmOTKhw,33
|
|
12
|
+
countably-2026.5.16.56074.dev1.dist-info/top_level.txt,sha256=mMlnfD_QFjgGEy1lC7pkT5K55QHhsqVpAGoWBu4VjX4,10
|
|
13
|
+
countably-2026.5.16.56074.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
countably
|