PythonIota 1.0.0__tar.gz → 1.1.0__tar.gz
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.
- {pythoniota-1.0.0 → pythoniota-1.1.0}/PKG-INFO +1 -1
- {pythoniota-1.0.0 → pythoniota-1.1.0}/pyproject.toml +1 -1
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/PythonIota.egg-info/PKG-INFO +1 -1
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/PythonIota.egg-info/SOURCES.txt +6 -1
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/__init__.py +5 -2
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/_bitflag.py +14 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/enum.py +90 -8
- pythoniota-1.1.0/src/pythoniota/recipes.py +236 -0
- pythoniota-1.1.0/tests/test_enum_enhanced.py +89 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_integration.py +1 -1
- pythoniota-1.1.0/tests/test_recipes.py +130 -0
- pythoniota-1.1.0/tests/test_serialization.py +86 -0
- pythoniota-1.1.0/tests/test_string_enum.py +114 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/setup.cfg +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/PythonIota.egg-info/top_level.txt +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/_compat.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/_safe_eval.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/src/pythoniota/sequence.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_bitflags.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_compat.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_enum.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_safe_eval.py +0 -0
- {pythoniota-1.0.0 → pythoniota-1.1.0}/tests/test_sequence.py +0 -0
|
@@ -8,10 +8,15 @@ src/pythoniota/_bitflag.py
|
|
|
8
8
|
src/pythoniota/_compat.py
|
|
9
9
|
src/pythoniota/_safe_eval.py
|
|
10
10
|
src/pythoniota/enum.py
|
|
11
|
+
src/pythoniota/recipes.py
|
|
11
12
|
src/pythoniota/sequence.py
|
|
12
13
|
tests/test_bitflags.py
|
|
13
14
|
tests/test_compat.py
|
|
14
15
|
tests/test_enum.py
|
|
16
|
+
tests/test_enum_enhanced.py
|
|
15
17
|
tests/test_integration.py
|
|
18
|
+
tests/test_recipes.py
|
|
16
19
|
tests/test_safe_eval.py
|
|
17
|
-
tests/test_sequence.py
|
|
20
|
+
tests/test_sequence.py
|
|
21
|
+
tests/test_serialization.py
|
|
22
|
+
tests/test_string_enum.py
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
from pythoniota._compat import Iota
|
|
2
2
|
from pythoniota._bitflag import BitFlag
|
|
3
3
|
from pythoniota._safe_eval import safe_eval
|
|
4
|
-
from pythoniota.enum import IotaEnum, IotaBitFlags
|
|
4
|
+
from pythoniota.enum import IotaEnum, IotaBitFlags, IotaStringEnum
|
|
5
5
|
from pythoniota.sequence import IotaSequence as iota
|
|
6
|
+
from pythoniota import recipes
|
|
6
7
|
|
|
7
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.1.0"
|
|
8
9
|
__all__ = [
|
|
9
10
|
"Iota",
|
|
10
11
|
"IotaEnum",
|
|
11
12
|
"IotaBitFlags",
|
|
13
|
+
"IotaStringEnum",
|
|
12
14
|
"iota",
|
|
13
15
|
"BitFlag",
|
|
14
16
|
"safe_eval",
|
|
17
|
+
"recipes",
|
|
15
18
|
]
|
|
@@ -69,6 +69,20 @@ class BitFlag:
|
|
|
69
69
|
def has(self, flag: BitFlag | int) -> bool:
|
|
70
70
|
return flag in self
|
|
71
71
|
|
|
72
|
+
def has_any(self, *flags: BitFlag | int) -> bool:
|
|
73
|
+
for f in flags:
|
|
74
|
+
fv = f._value if isinstance(f, BitFlag) else int(f)
|
|
75
|
+
if self._value & fv:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def has_all(self, *flags: BitFlag | int) -> bool:
|
|
80
|
+
for f in flags:
|
|
81
|
+
fv = f._value if isinstance(f, BitFlag) else int(f)
|
|
82
|
+
if (self._value & fv) != fv:
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
72
86
|
def decompose(self) -> list[BitFlag]:
|
|
73
87
|
result = []
|
|
74
88
|
v = self._value
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from collections import OrderedDict
|
|
4
5
|
from typing import Any, Iterator
|
|
5
6
|
|
|
@@ -124,7 +125,7 @@ class IotaNamespace(dict): # type: ignore[type-arg]
|
|
|
124
125
|
if (
|
|
125
126
|
not key.startswith("_")
|
|
126
127
|
and key != "iota"
|
|
127
|
-
and isinstance(value, (int, float, _IotaCounter))
|
|
128
|
+
and isinstance(value, (int, float, str, _IotaCounter))
|
|
128
129
|
):
|
|
129
130
|
if key not in self._member_names:
|
|
130
131
|
self._member_names.append(key)
|
|
@@ -144,25 +145,45 @@ class IotaEnumMeta(type):
|
|
|
144
145
|
**kwargs: Any,
|
|
145
146
|
) -> IotaEnumMeta:
|
|
146
147
|
member_names = namespace._member_names if isinstance(namespace, IotaNamespace) else []
|
|
147
|
-
members: OrderedDict[str,
|
|
148
|
+
members: OrderedDict[str, Any] = OrderedDict()
|
|
148
149
|
for mname in member_names:
|
|
149
150
|
val = namespace[mname] if mname in namespace else 0
|
|
150
151
|
members[mname] = int(val) if isinstance(val, _IotaCounter) else val
|
|
151
152
|
|
|
152
153
|
cls = super().__new__(mcs, name, bases, dict(namespace))
|
|
153
154
|
cls._members_ = members # type: ignore[attr-defined]
|
|
155
|
+
cls._aliases_: dict[str, str] = {} # type: ignore[attr-defined]
|
|
154
156
|
|
|
155
157
|
is_bitflag = any(
|
|
156
158
|
hasattr(b, "_is_bitflag_base") for b in bases
|
|
157
159
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
is_string_enum = any(
|
|
161
|
+
hasattr(b, "_is_string_enum_base") for b in bases
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if is_string_enum:
|
|
165
|
+
fmt = namespace.get("_format_", None)
|
|
166
|
+
for mname, mval in list(members.items()):
|
|
167
|
+
if isinstance(mval, _IotaCounter) or isinstance(mval, int):
|
|
168
|
+
if fmt:
|
|
169
|
+
members[mname] = fmt.format(name=mname, index=mval if isinstance(mval, int) else int(mval))
|
|
170
|
+
else:
|
|
171
|
+
members[mname] = mname
|
|
172
|
+
type.__setattr__(cls, mname, members[mname])
|
|
173
|
+
elif is_bitflag:
|
|
174
|
+
for mname, mval in members.items():
|
|
160
175
|
flag = BitFlag(int(mval), mname)
|
|
161
176
|
members[mname] = flag
|
|
162
177
|
type.__setattr__(cls, mname, flag)
|
|
163
|
-
|
|
178
|
+
else:
|
|
179
|
+
for mname, mval in members.items():
|
|
164
180
|
type.__setattr__(cls, mname, mval)
|
|
165
181
|
|
|
182
|
+
doc_lines = [f"{name} enum members:"]
|
|
183
|
+
for mname, mval in members.items():
|
|
184
|
+
doc_lines.append(f" {mname} = {mval}")
|
|
185
|
+
cls.__doc__ = "\n".join(doc_lines)
|
|
186
|
+
|
|
166
187
|
return cls
|
|
167
188
|
|
|
168
189
|
def __iter__(cls) -> Iterator[tuple[str, int | float]]:
|
|
@@ -201,20 +222,55 @@ class IotaEnum(metaclass=IotaEnumMeta):
|
|
|
201
222
|
return list(cls._members_.keys()) # type: ignore[attr-defined]
|
|
202
223
|
|
|
203
224
|
@classmethod
|
|
204
|
-
def values(cls) -> list[
|
|
225
|
+
def values(cls) -> list[Any]:
|
|
205
226
|
return list(cls._members_.values()) # type: ignore[attr-defined]
|
|
206
227
|
|
|
207
228
|
@classmethod
|
|
208
|
-
def items(cls) -> list[tuple[str,
|
|
229
|
+
def items(cls) -> list[tuple[str, Any]]:
|
|
209
230
|
return list(cls._members_.items()) # type: ignore[attr-defined]
|
|
210
231
|
|
|
211
232
|
@classmethod
|
|
212
|
-
def from_value(cls, value:
|
|
233
|
+
def from_value(cls, value: Any) -> str | None:
|
|
213
234
|
for name, val in cls._members_.items(): # type: ignore[attr-defined]
|
|
214
235
|
if val == value:
|
|
215
236
|
return name
|
|
237
|
+
for alias, target in cls._aliases_.items(): # type: ignore[attr-defined]
|
|
238
|
+
if cls._members_[target] == value: # type: ignore[attr-defined]
|
|
239
|
+
return alias
|
|
216
240
|
return None
|
|
217
241
|
|
|
242
|
+
@classmethod
|
|
243
|
+
def alias(cls, alias_name: str, target_name: str) -> None:
|
|
244
|
+
if target_name not in cls._members_: # type: ignore[attr-defined]
|
|
245
|
+
raise KeyError(f"'{target_name}' is not a member of {cls.__name__}")
|
|
246
|
+
cls._aliases_[alias_name] = target_name # type: ignore[attr-defined]
|
|
247
|
+
type.__setattr__(cls, alias_name, cls._members_[target_name]) # type: ignore[attr-defined]
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def to_dict(cls) -> dict[str, Any]:
|
|
251
|
+
return dict(cls._members_) # type: ignore[attr-defined]
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def to_json(cls, **kwargs: Any) -> str:
|
|
255
|
+
d = {}
|
|
256
|
+
for k, v in cls._members_.items(): # type: ignore[attr-defined]
|
|
257
|
+
d[k] = int(v) if isinstance(v, (BitFlag, float)) and not isinstance(v, str) else v
|
|
258
|
+
return json.dumps(d, **kwargs)
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
262
|
+
result = {}
|
|
263
|
+
for name, value in data.items():
|
|
264
|
+
if name in cls._members_: # type: ignore[attr-defined]
|
|
265
|
+
result[name] = cls._members_[name] # type: ignore[attr-defined]
|
|
266
|
+
else:
|
|
267
|
+
raise KeyError(f"'{name}' is not a member of {cls.__name__}")
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def from_json(cls, s: str) -> dict[str, Any]:
|
|
272
|
+
return cls.from_dict(json.loads(s))
|
|
273
|
+
|
|
218
274
|
|
|
219
275
|
class IotaBitFlags(metaclass=IotaEnumMeta):
|
|
220
276
|
_is_bitflag_base = True
|
|
@@ -237,3 +293,29 @@ class IotaBitFlags(metaclass=IotaEnumMeta):
|
|
|
237
293
|
if int(flag) == value:
|
|
238
294
|
return name
|
|
239
295
|
return None
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def to_dict(cls) -> dict[str, int]:
|
|
299
|
+
return {k: int(v) for k, v in cls._members_.items()} # type: ignore[attr-defined]
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def to_json(cls, **kwargs: Any) -> str:
|
|
303
|
+
return json.dumps(cls.to_dict(), **kwargs)
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def from_dict(cls, data: dict[str, Any]) -> dict[str, BitFlag]:
|
|
307
|
+
result = {}
|
|
308
|
+
for name in data:
|
|
309
|
+
if name in cls._members_: # type: ignore[attr-defined]
|
|
310
|
+
result[name] = cls._members_[name] # type: ignore[attr-defined]
|
|
311
|
+
else:
|
|
312
|
+
raise KeyError(f"'{name}' is not a member of {cls.__name__}")
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def from_json(cls, s: str) -> dict[str, BitFlag]:
|
|
317
|
+
return cls.from_dict(json.loads(s))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class IotaStringEnum(IotaEnum):
|
|
321
|
+
_is_string_enum_base = True
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import math
|
|
5
|
+
from typing import Any, Callable, Iterator
|
|
6
|
+
|
|
7
|
+
from pythoniota.sequence import IotaSequence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fibonacci(n: int | None = None) -> IotaSequence:
|
|
11
|
+
def _fib_map(_: int) -> int:
|
|
12
|
+
return 0 # placeholder, overridden below
|
|
13
|
+
|
|
14
|
+
class FibSequence(IotaSequence):
|
|
15
|
+
def __iter__(self) -> Iterator[int]:
|
|
16
|
+
a, b = 0, 1
|
|
17
|
+
if self._stop is None:
|
|
18
|
+
while True:
|
|
19
|
+
yield a
|
|
20
|
+
a, b = b, a + b
|
|
21
|
+
else:
|
|
22
|
+
count = 0
|
|
23
|
+
while count < self._stop:
|
|
24
|
+
yield a
|
|
25
|
+
a, b = b, a + b
|
|
26
|
+
count += 1
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
if self._stop is None:
|
|
30
|
+
return "fibonacci(infinite)"
|
|
31
|
+
return f"fibonacci({self._stop})"
|
|
32
|
+
|
|
33
|
+
seq = FibSequence.__new__(FibSequence)
|
|
34
|
+
seq._start = 0
|
|
35
|
+
seq._stop = n
|
|
36
|
+
seq._step = 1
|
|
37
|
+
seq._map = None
|
|
38
|
+
return seq
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def geometric(start: int | float, ratio: int | float, n: int | None = None) -> IotaSequence:
|
|
42
|
+
class GeomSequence(IotaSequence):
|
|
43
|
+
def __init__(self, start_val: int | float, ratio_val: int | float, count: int | None) -> None:
|
|
44
|
+
self._start = 0
|
|
45
|
+
self._stop = count
|
|
46
|
+
self._step = 1
|
|
47
|
+
self._map = None
|
|
48
|
+
self._geo_start = start_val
|
|
49
|
+
self._geo_ratio = ratio_val
|
|
50
|
+
|
|
51
|
+
def __iter__(self) -> Iterator[int | float]:
|
|
52
|
+
val = self._geo_start
|
|
53
|
+
if self._stop is None:
|
|
54
|
+
while True:
|
|
55
|
+
yield val
|
|
56
|
+
val *= self._geo_ratio
|
|
57
|
+
else:
|
|
58
|
+
for _ in range(self._stop):
|
|
59
|
+
yield val
|
|
60
|
+
val *= self._geo_ratio
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
if self._stop is None:
|
|
64
|
+
return f"geometric(start={self._geo_start}, ratio={self._geo_ratio}, infinite)"
|
|
65
|
+
return f"geometric(start={self._geo_start}, ratio={self._geo_ratio}, n={self._stop})"
|
|
66
|
+
|
|
67
|
+
return GeomSequence(start, ratio, n)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def primes(n: int | None = None) -> IotaSequence:
|
|
71
|
+
class PrimeSequence(IotaSequence):
|
|
72
|
+
def __iter__(self) -> Iterator[int]:
|
|
73
|
+
count = 0
|
|
74
|
+
candidate = 2
|
|
75
|
+
while n is None or count < n:
|
|
76
|
+
if _is_prime(candidate):
|
|
77
|
+
yield candidate
|
|
78
|
+
count += 1
|
|
79
|
+
candidate += 1
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
if self._stop is None:
|
|
83
|
+
return "primes(infinite)"
|
|
84
|
+
return f"primes({self._stop})"
|
|
85
|
+
|
|
86
|
+
seq = PrimeSequence.__new__(PrimeSequence)
|
|
87
|
+
seq._start = 0
|
|
88
|
+
seq._stop = n
|
|
89
|
+
seq._step = 1
|
|
90
|
+
seq._map = None
|
|
91
|
+
return seq
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_prime(num: int) -> bool:
|
|
95
|
+
if num < 2:
|
|
96
|
+
return False
|
|
97
|
+
if num < 4:
|
|
98
|
+
return True
|
|
99
|
+
if num % 2 == 0 or num % 3 == 0:
|
|
100
|
+
return False
|
|
101
|
+
i = 5
|
|
102
|
+
while i * i <= num:
|
|
103
|
+
if num % i == 0 or num % (i + 2) == 0:
|
|
104
|
+
return False
|
|
105
|
+
i += 6
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def repeat(value: Any, n: int | None = None) -> IotaSequence:
|
|
110
|
+
class RepeatSequence(IotaSequence):
|
|
111
|
+
def __init__(self, val: Any, count: int | None) -> None:
|
|
112
|
+
self._start = 0
|
|
113
|
+
self._stop = count
|
|
114
|
+
self._step = 1
|
|
115
|
+
self._map = None
|
|
116
|
+
self._repeat_val = val
|
|
117
|
+
|
|
118
|
+
def __iter__(self) -> Iterator[Any]:
|
|
119
|
+
if self._stop is None:
|
|
120
|
+
while True:
|
|
121
|
+
yield self._repeat_val
|
|
122
|
+
else:
|
|
123
|
+
for _ in range(self._stop):
|
|
124
|
+
yield self._repeat_val
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
if self._stop is None:
|
|
128
|
+
return f"repeat({self._repeat_val!r}, infinite)"
|
|
129
|
+
return f"repeat({self._repeat_val!r}, {self._stop})"
|
|
130
|
+
|
|
131
|
+
return RepeatSequence(value, n)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cycle(iterable: Any, n: int | None = None) -> IotaSequence:
|
|
135
|
+
class CycleSequence(IotaSequence):
|
|
136
|
+
def __init__(self, items: Any, count: int | None) -> None:
|
|
137
|
+
self._start = 0
|
|
138
|
+
self._stop = count
|
|
139
|
+
self._step = 1
|
|
140
|
+
self._map = None
|
|
141
|
+
self._cycle_items = list(items)
|
|
142
|
+
|
|
143
|
+
def __iter__(self) -> Iterator[Any]:
|
|
144
|
+
if not self._cycle_items:
|
|
145
|
+
return
|
|
146
|
+
if self._stop is None:
|
|
147
|
+
while True:
|
|
148
|
+
yield from self._cycle_items
|
|
149
|
+
else:
|
|
150
|
+
emitted = 0
|
|
151
|
+
while emitted < self._stop:
|
|
152
|
+
for item in self._cycle_items:
|
|
153
|
+
if emitted >= self._stop:
|
|
154
|
+
return
|
|
155
|
+
yield item
|
|
156
|
+
emitted += 1
|
|
157
|
+
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
if self._stop is None:
|
|
160
|
+
return f"cycle({self._cycle_items!r}, infinite)"
|
|
161
|
+
return f"cycle({self._cycle_items!r}, {self._stop})"
|
|
162
|
+
|
|
163
|
+
return CycleSequence(iterable, n)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def collatz(start: int) -> IotaSequence:
|
|
167
|
+
class CollatzSequence(IotaSequence):
|
|
168
|
+
def __init__(self, start_val: int) -> None:
|
|
169
|
+
self._start = 0
|
|
170
|
+
self._stop = None
|
|
171
|
+
self._step = 1
|
|
172
|
+
self._map = None
|
|
173
|
+
self._collatz_start = start_val
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def infinite(self) -> bool:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def __iter__(self) -> Iterator[int]:
|
|
180
|
+
n = self._collatz_start
|
|
181
|
+
yield n
|
|
182
|
+
while n != 1:
|
|
183
|
+
n = n // 2 if n % 2 == 0 else 3 * n + 1
|
|
184
|
+
yield n
|
|
185
|
+
|
|
186
|
+
def __len__(self) -> int:
|
|
187
|
+
return sum(1 for _ in self)
|
|
188
|
+
|
|
189
|
+
def __repr__(self) -> str:
|
|
190
|
+
return f"collatz({self._collatz_start})"
|
|
191
|
+
|
|
192
|
+
return CollatzSequence(start)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def triangle(n: int | None = None) -> IotaSequence:
|
|
196
|
+
if n is not None:
|
|
197
|
+
return IotaSequence(n, map=lambda i: i * (i + 1) // 2)
|
|
198
|
+
|
|
199
|
+
class TriSequence(IotaSequence):
|
|
200
|
+
def __iter__(self) -> Iterator[int]:
|
|
201
|
+
i = 0
|
|
202
|
+
while True:
|
|
203
|
+
yield i * (i + 1) // 2
|
|
204
|
+
i += 1
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return "triangle(infinite)"
|
|
208
|
+
|
|
209
|
+
seq = TriSequence.__new__(TriSequence)
|
|
210
|
+
seq._start = 0
|
|
211
|
+
seq._stop = None
|
|
212
|
+
seq._step = 1
|
|
213
|
+
seq._map = None
|
|
214
|
+
return seq
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def powers(base: int | float, n: int | None = None) -> IotaSequence:
|
|
218
|
+
if n is not None:
|
|
219
|
+
return IotaSequence(n, map=lambda i: base ** i)
|
|
220
|
+
|
|
221
|
+
class PowerSequence(IotaSequence):
|
|
222
|
+
def __iter__(self) -> Iterator[int | float]:
|
|
223
|
+
i = 0
|
|
224
|
+
while True:
|
|
225
|
+
yield base ** i
|
|
226
|
+
i += 1
|
|
227
|
+
|
|
228
|
+
def __repr__(self) -> str:
|
|
229
|
+
return f"powers({base}, infinite)"
|
|
230
|
+
|
|
231
|
+
seq = PowerSequence.__new__(PowerSequence)
|
|
232
|
+
seq._start = 0
|
|
233
|
+
seq._stop = None
|
|
234
|
+
seq._step = 1
|
|
235
|
+
seq._map = None
|
|
236
|
+
return seq
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pythoniota._bitflag import BitFlag
|
|
3
|
+
from pythoniota.enum import IotaEnum, IotaBitFlags
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestBitFlagHasAnyAll:
|
|
7
|
+
def test_has_any_true(self):
|
|
8
|
+
combined = BitFlag(7)
|
|
9
|
+
assert combined.has_any(BitFlag(1), BitFlag(8))
|
|
10
|
+
|
|
11
|
+
def test_has_any_false(self):
|
|
12
|
+
combined = BitFlag(6)
|
|
13
|
+
assert not combined.has_any(BitFlag(1), BitFlag(8))
|
|
14
|
+
|
|
15
|
+
def test_has_all_true(self):
|
|
16
|
+
combined = BitFlag(7)
|
|
17
|
+
assert combined.has_all(BitFlag(1), BitFlag(2), BitFlag(4))
|
|
18
|
+
|
|
19
|
+
def test_has_all_false(self):
|
|
20
|
+
combined = BitFlag(3)
|
|
21
|
+
assert not combined.has_all(BitFlag(1), BitFlag(4))
|
|
22
|
+
|
|
23
|
+
def test_has_any_with_int(self):
|
|
24
|
+
combined = BitFlag(7)
|
|
25
|
+
assert combined.has_any(1, 8)
|
|
26
|
+
|
|
27
|
+
def test_has_all_with_int(self):
|
|
28
|
+
combined = BitFlag(7)
|
|
29
|
+
assert combined.has_all(1, 2, 4)
|
|
30
|
+
|
|
31
|
+
def test_has_any_with_bitflags_enum(self):
|
|
32
|
+
class Perm(IotaBitFlags):
|
|
33
|
+
R = 1 << iota # noqa: F821
|
|
34
|
+
W = 1 << iota # noqa: F821
|
|
35
|
+
X = 1 << iota # noqa: F821
|
|
36
|
+
|
|
37
|
+
rw = Perm.R | Perm.W
|
|
38
|
+
assert rw.has_any(Perm.R, Perm.X)
|
|
39
|
+
assert not rw.has_any(Perm.X)
|
|
40
|
+
|
|
41
|
+
def test_has_all_with_bitflags_enum(self):
|
|
42
|
+
class Perm(IotaBitFlags):
|
|
43
|
+
R = 1 << iota # noqa: F821
|
|
44
|
+
W = 1 << iota # noqa: F821
|
|
45
|
+
X = 1 << iota # noqa: F821
|
|
46
|
+
|
|
47
|
+
rwx = Perm.R | Perm.W | Perm.X
|
|
48
|
+
assert rwx.has_all(Perm.R, Perm.W)
|
|
49
|
+
assert rwx.has_all(Perm.R, Perm.W, Perm.X)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestEnumAlias:
|
|
53
|
+
def test_alias(self):
|
|
54
|
+
class Color(IotaEnum):
|
|
55
|
+
Red = iota # noqa: F821
|
|
56
|
+
Green = iota # noqa: F821
|
|
57
|
+
Blue = iota # noqa: F821
|
|
58
|
+
|
|
59
|
+
Color.alias("R", "Red")
|
|
60
|
+
Color.alias("G", "Green")
|
|
61
|
+
assert Color.R == 0
|
|
62
|
+
assert Color.G == 1
|
|
63
|
+
|
|
64
|
+
def test_alias_invalid_target(self):
|
|
65
|
+
class Color(IotaEnum):
|
|
66
|
+
Red = iota # noqa: F821
|
|
67
|
+
|
|
68
|
+
with pytest.raises(KeyError, match="not a member"):
|
|
69
|
+
Color.alias("X", "Purple")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestAutoDocstring:
|
|
73
|
+
def test_enum_has_docstring(self):
|
|
74
|
+
class Color(IotaEnum):
|
|
75
|
+
Red = iota # noqa: F821
|
|
76
|
+
Green = iota # noqa: F821
|
|
77
|
+
|
|
78
|
+
assert Color.__doc__ is not None
|
|
79
|
+
assert "Red" in Color.__doc__
|
|
80
|
+
assert "Green" in Color.__doc__
|
|
81
|
+
|
|
82
|
+
def test_bitflags_has_docstring(self):
|
|
83
|
+
class Perm(IotaBitFlags):
|
|
84
|
+
Read = 1 << iota # noqa: F821
|
|
85
|
+
Write = 1 << iota # noqa: F821
|
|
86
|
+
|
|
87
|
+
assert Perm.__doc__ is not None
|
|
88
|
+
assert "Read" in Perm.__doc__
|
|
89
|
+
assert "Write" in Perm.__doc__
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pythoniota.recipes import (
|
|
3
|
+
fibonacci, geometric, primes, repeat, cycle, collatz, triangle, powers
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestFibonacci:
|
|
8
|
+
def test_first_10(self):
|
|
9
|
+
assert fibonacci(10).take(10) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
|
10
|
+
|
|
11
|
+
def test_first_1(self):
|
|
12
|
+
assert list(fibonacci(1)) == [0]
|
|
13
|
+
|
|
14
|
+
def test_empty(self):
|
|
15
|
+
assert list(fibonacci(0)) == []
|
|
16
|
+
|
|
17
|
+
def test_infinite_take(self):
|
|
18
|
+
assert fibonacci().take(7) == [0, 1, 1, 2, 3, 5, 8]
|
|
19
|
+
|
|
20
|
+
def test_repr(self):
|
|
21
|
+
assert "fibonacci" in repr(fibonacci(5))
|
|
22
|
+
assert "infinite" in repr(fibonacci())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestGeometric:
|
|
26
|
+
def test_basic(self):
|
|
27
|
+
assert list(geometric(1, 2, 5)) == [1, 2, 4, 8, 16]
|
|
28
|
+
|
|
29
|
+
def test_fraction(self):
|
|
30
|
+
result = list(geometric(100, 0.5, 4))
|
|
31
|
+
assert result == [100, 50.0, 25.0, 12.5]
|
|
32
|
+
|
|
33
|
+
def test_infinite_take(self):
|
|
34
|
+
assert geometric(1, 3).take(4) == [1, 3, 9, 27]
|
|
35
|
+
|
|
36
|
+
def test_repr(self):
|
|
37
|
+
assert "geometric" in repr(geometric(1, 2, 5))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestPrimes:
|
|
41
|
+
def test_first_10(self):
|
|
42
|
+
assert primes(10).take(10) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
|
|
43
|
+
|
|
44
|
+
def test_first_1(self):
|
|
45
|
+
assert list(primes(1)) == [2]
|
|
46
|
+
|
|
47
|
+
def test_infinite_take(self):
|
|
48
|
+
assert primes().take(5) == [2, 3, 5, 7, 11]
|
|
49
|
+
|
|
50
|
+
def test_repr(self):
|
|
51
|
+
assert "primes" in repr(primes(5))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestRepeat:
|
|
55
|
+
def test_finite(self):
|
|
56
|
+
assert list(repeat("x", 3)) == ["x", "x", "x"]
|
|
57
|
+
|
|
58
|
+
def test_infinite_take(self):
|
|
59
|
+
assert repeat(42).take(4) == [42, 42, 42, 42]
|
|
60
|
+
|
|
61
|
+
def test_empty(self):
|
|
62
|
+
assert list(repeat("x", 0)) == []
|
|
63
|
+
|
|
64
|
+
def test_repr(self):
|
|
65
|
+
assert "repeat" in repr(repeat(1, 3))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestCycle:
|
|
69
|
+
def test_finite(self):
|
|
70
|
+
assert list(cycle([1, 2, 3], 7)) == [1, 2, 3, 1, 2, 3, 1]
|
|
71
|
+
|
|
72
|
+
def test_infinite_take(self):
|
|
73
|
+
assert cycle(["a", "b"]).take(5) == ["a", "b", "a", "b", "a"]
|
|
74
|
+
|
|
75
|
+
def test_empty_iterable(self):
|
|
76
|
+
assert list(cycle([], 5)) == []
|
|
77
|
+
|
|
78
|
+
def test_exact_multiple(self):
|
|
79
|
+
assert list(cycle([1, 2], 4)) == [1, 2, 1, 2]
|
|
80
|
+
|
|
81
|
+
def test_repr(self):
|
|
82
|
+
assert "cycle" in repr(cycle([1], 3))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestCollatz:
|
|
86
|
+
def test_from_6(self):
|
|
87
|
+
assert list(collatz(6)) == [6, 3, 10, 5, 16, 8, 4, 2, 1]
|
|
88
|
+
|
|
89
|
+
def test_from_1(self):
|
|
90
|
+
assert list(collatz(1)) == [1]
|
|
91
|
+
|
|
92
|
+
def test_from_27(self):
|
|
93
|
+
result = list(collatz(27))
|
|
94
|
+
assert result[0] == 27
|
|
95
|
+
assert result[-1] == 1
|
|
96
|
+
assert len(result) == 112
|
|
97
|
+
|
|
98
|
+
def test_len(self):
|
|
99
|
+
assert len(collatz(6)) == 9
|
|
100
|
+
|
|
101
|
+
def test_repr(self):
|
|
102
|
+
assert "collatz" in repr(collatz(6))
|
|
103
|
+
|
|
104
|
+
def test_not_infinite(self):
|
|
105
|
+
assert collatz(6).infinite is False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestTriangle:
|
|
109
|
+
def test_first_6(self):
|
|
110
|
+
assert list(triangle(6)) == [0, 1, 3, 6, 10, 15]
|
|
111
|
+
|
|
112
|
+
def test_infinite_take(self):
|
|
113
|
+
assert triangle().take(5) == [0, 1, 3, 6, 10]
|
|
114
|
+
|
|
115
|
+
def test_repr(self):
|
|
116
|
+
assert "triangle" in repr(triangle())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestPowers:
|
|
120
|
+
def test_powers_of_2(self):
|
|
121
|
+
assert list(powers(2, 5)) == [1, 2, 4, 8, 16]
|
|
122
|
+
|
|
123
|
+
def test_powers_of_3(self):
|
|
124
|
+
assert list(powers(3, 4)) == [1, 3, 9, 27]
|
|
125
|
+
|
|
126
|
+
def test_infinite_take(self):
|
|
127
|
+
assert powers(2).take(4) == [1, 2, 4, 8]
|
|
128
|
+
|
|
129
|
+
def test_repr(self):
|
|
130
|
+
assert "powers" in repr(powers(2))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
from pythoniota.enum import IotaEnum, IotaBitFlags
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestEnumSerialization:
|
|
7
|
+
def test_to_dict(self):
|
|
8
|
+
class Color(IotaEnum):
|
|
9
|
+
Red = iota # noqa: F821
|
|
10
|
+
Green = iota # noqa: F821
|
|
11
|
+
Blue = iota # noqa: F821
|
|
12
|
+
|
|
13
|
+
assert Color.to_dict() == {"Red": 0, "Green": 1, "Blue": 2}
|
|
14
|
+
|
|
15
|
+
def test_to_json(self):
|
|
16
|
+
class Color(IotaEnum):
|
|
17
|
+
Red = iota # noqa: F821
|
|
18
|
+
Green = iota # noqa: F821
|
|
19
|
+
|
|
20
|
+
j = Color.to_json()
|
|
21
|
+
assert json.loads(j) == {"Red": 0, "Green": 1}
|
|
22
|
+
|
|
23
|
+
def test_to_json_indent(self):
|
|
24
|
+
class X(IotaEnum):
|
|
25
|
+
A = iota # noqa: F821
|
|
26
|
+
|
|
27
|
+
j = X.to_json(indent=2)
|
|
28
|
+
assert "\n" in j
|
|
29
|
+
|
|
30
|
+
def test_from_dict(self):
|
|
31
|
+
class Color(IotaEnum):
|
|
32
|
+
Red = iota # noqa: F821
|
|
33
|
+
Green = iota # noqa: F821
|
|
34
|
+
|
|
35
|
+
result = Color.from_dict({"Red": 0})
|
|
36
|
+
assert result == {"Red": 0}
|
|
37
|
+
|
|
38
|
+
def test_from_dict_invalid_key(self):
|
|
39
|
+
class Color(IotaEnum):
|
|
40
|
+
Red = iota # noqa: F821
|
|
41
|
+
|
|
42
|
+
with pytest.raises(KeyError, match="not a member"):
|
|
43
|
+
Color.from_dict({"Purple": 99})
|
|
44
|
+
|
|
45
|
+
def test_from_json(self):
|
|
46
|
+
class Color(IotaEnum):
|
|
47
|
+
Red = iota # noqa: F821
|
|
48
|
+
Green = iota # noqa: F821
|
|
49
|
+
|
|
50
|
+
result = Color.from_json('{"Red": 0, "Green": 1}')
|
|
51
|
+
assert result == {"Red": 0, "Green": 1}
|
|
52
|
+
|
|
53
|
+
def test_roundtrip(self):
|
|
54
|
+
class Status(IotaEnum):
|
|
55
|
+
OK = iota # noqa: F821
|
|
56
|
+
Error = iota # noqa: F821
|
|
57
|
+
Pending = iota # noqa: F821
|
|
58
|
+
|
|
59
|
+
j = Status.to_json()
|
|
60
|
+
restored = Status.from_json(j)
|
|
61
|
+
assert restored == Status.to_dict()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestBitFlagsSerialization:
|
|
65
|
+
def test_to_dict(self):
|
|
66
|
+
class Perm(IotaBitFlags):
|
|
67
|
+
Read = 1 << iota # noqa: F821
|
|
68
|
+
Write = 1 << iota # noqa: F821
|
|
69
|
+
|
|
70
|
+
assert Perm.to_dict() == {"Read": 1, "Write": 2}
|
|
71
|
+
|
|
72
|
+
def test_to_json(self):
|
|
73
|
+
class Perm(IotaBitFlags):
|
|
74
|
+
Read = 1 << iota # noqa: F821
|
|
75
|
+
Write = 1 << iota # noqa: F821
|
|
76
|
+
|
|
77
|
+
j = Perm.to_json()
|
|
78
|
+
assert json.loads(j) == {"Read": 1, "Write": 2}
|
|
79
|
+
|
|
80
|
+
def test_from_json(self):
|
|
81
|
+
class Perm(IotaBitFlags):
|
|
82
|
+
Read = 1 << iota # noqa: F821
|
|
83
|
+
Write = 1 << iota # noqa: F821
|
|
84
|
+
|
|
85
|
+
result = Perm.from_json('{"Read": 1}')
|
|
86
|
+
assert "Read" in result
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pythoniota.enum import IotaStringEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestIotaStringEnum:
|
|
6
|
+
def test_basic_string_enum(self):
|
|
7
|
+
class Color(IotaStringEnum):
|
|
8
|
+
Red = iota # noqa: F821
|
|
9
|
+
Green = iota # noqa: F821
|
|
10
|
+
Blue = iota # noqa: F821
|
|
11
|
+
|
|
12
|
+
assert Color.Red == "Red"
|
|
13
|
+
assert Color.Green == "Green"
|
|
14
|
+
assert Color.Blue == "Blue"
|
|
15
|
+
|
|
16
|
+
def test_custom_format(self):
|
|
17
|
+
class Color(IotaStringEnum):
|
|
18
|
+
_format_ = "color_{name}"
|
|
19
|
+
Red = iota # noqa: F821
|
|
20
|
+
Green = iota # noqa: F821
|
|
21
|
+
|
|
22
|
+
assert Color.Red == "color_Red"
|
|
23
|
+
assert Color.Green == "color_Green"
|
|
24
|
+
|
|
25
|
+
def test_format_with_index(self):
|
|
26
|
+
class Code(IotaStringEnum):
|
|
27
|
+
_format_ = "{name}_{index:03d}"
|
|
28
|
+
OK = iota # noqa: F821
|
|
29
|
+
Error = iota # noqa: F821
|
|
30
|
+
Warn = iota # noqa: F821
|
|
31
|
+
|
|
32
|
+
assert Code.OK == "OK_000"
|
|
33
|
+
assert Code.Error == "Error_001"
|
|
34
|
+
assert Code.Warn == "Warn_002"
|
|
35
|
+
|
|
36
|
+
def test_explicit_string_value(self):
|
|
37
|
+
class Lang(IotaStringEnum):
|
|
38
|
+
Python = "py"
|
|
39
|
+
Rust = "rs"
|
|
40
|
+
|
|
41
|
+
assert Lang.Python == "py"
|
|
42
|
+
assert Lang.Rust == "rs"
|
|
43
|
+
|
|
44
|
+
def test_mixed_iota_and_explicit(self):
|
|
45
|
+
class Mix(IotaStringEnum):
|
|
46
|
+
Auto = iota # noqa: F821
|
|
47
|
+
Manual = "custom_value"
|
|
48
|
+
|
|
49
|
+
assert Mix.Auto == "Auto"
|
|
50
|
+
assert Mix.Manual == "custom_value"
|
|
51
|
+
|
|
52
|
+
def test_iteration(self):
|
|
53
|
+
class Dir(IotaStringEnum):
|
|
54
|
+
North = iota # noqa: F821
|
|
55
|
+
South = iota # noqa: F821
|
|
56
|
+
|
|
57
|
+
items = list(Dir)
|
|
58
|
+
assert items == [("North", "North"), ("South", "South")]
|
|
59
|
+
|
|
60
|
+
def test_names(self):
|
|
61
|
+
class X(IotaStringEnum):
|
|
62
|
+
A = iota # noqa: F821
|
|
63
|
+
B = iota # noqa: F821
|
|
64
|
+
|
|
65
|
+
assert X.names() == ["A", "B"]
|
|
66
|
+
|
|
67
|
+
def test_values(self):
|
|
68
|
+
class X(IotaStringEnum):
|
|
69
|
+
A = iota # noqa: F821
|
|
70
|
+
B = iota # noqa: F821
|
|
71
|
+
|
|
72
|
+
assert X.values() == ["A", "B"]
|
|
73
|
+
|
|
74
|
+
def test_to_dict(self):
|
|
75
|
+
class X(IotaStringEnum):
|
|
76
|
+
A = iota # noqa: F821
|
|
77
|
+
B = iota # noqa: F821
|
|
78
|
+
|
|
79
|
+
assert X.to_dict() == {"A": "A", "B": "B"}
|
|
80
|
+
|
|
81
|
+
def test_to_json(self):
|
|
82
|
+
class X(IotaStringEnum):
|
|
83
|
+
A = iota # noqa: F821
|
|
84
|
+
|
|
85
|
+
import json
|
|
86
|
+
assert json.loads(X.to_json()) == {"A": "A"}
|
|
87
|
+
|
|
88
|
+
def test_from_value(self):
|
|
89
|
+
class X(IotaStringEnum):
|
|
90
|
+
A = iota # noqa: F821
|
|
91
|
+
B = iota # noqa: F821
|
|
92
|
+
|
|
93
|
+
assert X.from_value("A") == "A"
|
|
94
|
+
assert X.from_value("Z") is None
|
|
95
|
+
|
|
96
|
+
def test_immutable(self):
|
|
97
|
+
class X(IotaStringEnum):
|
|
98
|
+
A = iota # noqa: F821
|
|
99
|
+
|
|
100
|
+
with pytest.raises(AttributeError):
|
|
101
|
+
X.A = "changed"
|
|
102
|
+
|
|
103
|
+
def test_contains(self):
|
|
104
|
+
class X(IotaStringEnum):
|
|
105
|
+
A = iota # noqa: F821
|
|
106
|
+
|
|
107
|
+
assert "A" in X
|
|
108
|
+
|
|
109
|
+
def test_auto_docstring(self):
|
|
110
|
+
class X(IotaStringEnum):
|
|
111
|
+
A = iota # noqa: F821
|
|
112
|
+
|
|
113
|
+
assert X.__doc__ is not None
|
|
114
|
+
assert "A" in X.__doc__
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|