PythonIota 1.1.0__tar.gz → 1.2.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.1.0 → pythoniota-1.2.0}/PKG-INFO +1 -1
- {pythoniota-1.1.0 → pythoniota-1.2.0}/pyproject.toml +1 -1
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/PKG-INFO +1 -1
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/SOURCES.txt +1 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/__init__.py +7 -2
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_bitflag.py +14 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/enum.py +220 -1
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/sequence.py +138 -0
- pythoniota-1.2.0/tests/test_advanced.py +377 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_integration.py +1 -1
- {pythoniota-1.1.0 → pythoniota-1.2.0}/setup.cfg +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/top_level.txt +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_compat.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_safe_eval.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/recipes.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_bitflags.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_compat.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_enum.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_enum_enhanced.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_recipes.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_safe_eval.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_sequence.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_serialization.py +0 -0
- {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_string_enum.py +0 -0
|
@@ -1,11 +1,14 @@
|
|
|
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
|
|
4
|
+
from pythoniota.enum import (
|
|
5
|
+
IotaEnum, IotaBitFlags, IotaStringEnum,
|
|
6
|
+
FlagScope, iota_enum, _EnumMemberProxy,
|
|
7
|
+
)
|
|
5
8
|
from pythoniota.sequence import IotaSequence as iota
|
|
6
9
|
from pythoniota import recipes
|
|
7
10
|
|
|
8
|
-
__version__ = "1.
|
|
11
|
+
__version__ = "1.2.0"
|
|
9
12
|
__all__ = [
|
|
10
13
|
"Iota",
|
|
11
14
|
"IotaEnum",
|
|
@@ -13,6 +16,8 @@ __all__ = [
|
|
|
13
16
|
"IotaStringEnum",
|
|
14
17
|
"iota",
|
|
15
18
|
"BitFlag",
|
|
19
|
+
"FlagScope",
|
|
20
|
+
"iota_enum",
|
|
16
21
|
"safe_eval",
|
|
17
22
|
"recipes",
|
|
18
23
|
]
|
|
@@ -101,5 +101,19 @@ class BitFlag:
|
|
|
101
101
|
def __str__(self) -> str:
|
|
102
102
|
return self._name or str(self._value)
|
|
103
103
|
|
|
104
|
+
def __format__(self, format_spec: str) -> str:
|
|
105
|
+
if not format_spec:
|
|
106
|
+
return self._name or str(self._value)
|
|
107
|
+
if format_spec == "v":
|
|
108
|
+
return str(self._value)
|
|
109
|
+
if format_spec == "n":
|
|
110
|
+
return self._name
|
|
111
|
+
if format_spec == "b":
|
|
112
|
+
return bin(self._value)
|
|
113
|
+
return format(self._value, format_spec)
|
|
114
|
+
|
|
115
|
+
def __reduce__(self) -> tuple:
|
|
116
|
+
return (BitFlag, (self._value, self._name))
|
|
117
|
+
|
|
104
118
|
def __iter__(self) -> Iterator[BitFlag]:
|
|
105
119
|
return iter(self.decompose())
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import pickle
|
|
4
5
|
from collections import OrderedDict
|
|
5
|
-
from
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any, Callable, Iterator
|
|
6
8
|
|
|
7
9
|
from pythoniota._bitflag import BitFlag
|
|
8
10
|
|
|
@@ -161,6 +163,10 @@ class IotaEnumMeta(type):
|
|
|
161
163
|
hasattr(b, "_is_string_enum_base") for b in bases
|
|
162
164
|
)
|
|
163
165
|
|
|
166
|
+
is_ordered = namespace.get("_ordered_", False) or any(
|
|
167
|
+
getattr(b, "_ordered_", False) for b in bases
|
|
168
|
+
)
|
|
169
|
+
|
|
164
170
|
if is_string_enum:
|
|
165
171
|
fmt = namespace.get("_format_", None)
|
|
166
172
|
for mname, mval in list(members.items()):
|
|
@@ -175,6 +181,11 @@ class IotaEnumMeta(type):
|
|
|
175
181
|
flag = BitFlag(int(mval), mname)
|
|
176
182
|
members[mname] = flag
|
|
177
183
|
type.__setattr__(cls, mname, flag)
|
|
184
|
+
elif is_ordered:
|
|
185
|
+
for order, (mname, mval) in builtins_enumerate(members.items()):
|
|
186
|
+
proxy = _EnumMemberProxy(mname, mval, cls, order)
|
|
187
|
+
members[mname] = proxy
|
|
188
|
+
type.__setattr__(cls, mname, proxy)
|
|
178
189
|
else:
|
|
179
190
|
for mname, mval in members.items():
|
|
180
191
|
type.__setattr__(cls, mname, mval)
|
|
@@ -210,10 +221,148 @@ class IotaEnumMeta(type):
|
|
|
210
221
|
raise AttributeError(f"Cannot delete enum member '{name}'")
|
|
211
222
|
super().__delattr__(name)
|
|
212
223
|
|
|
224
|
+
def __call__(cls, value: Any) -> Any:
|
|
225
|
+
for name, val in cls._members_.items(): # type: ignore[attr-defined]
|
|
226
|
+
cmp_val = int(val) if isinstance(val, BitFlag) else val
|
|
227
|
+
if cmp_val == value:
|
|
228
|
+
return val
|
|
229
|
+
raise ValueError(f"{value!r} is not a valid value in {cls.__name__}")
|
|
230
|
+
|
|
213
231
|
def __repr__(cls) -> str:
|
|
214
232
|
items = ", ".join(f"{k}={v}" for k, v in cls._members_.items()) # type: ignore[attr-defined]
|
|
215
233
|
return f"<{cls.__name__}: {items}>"
|
|
216
234
|
|
|
235
|
+
def __reduce__(cls) -> tuple[Any, ...]:
|
|
236
|
+
return (cls.__class__, (cls.__name__, cls.__bases__, dict(cls._members_))) # type: ignore[attr-defined]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _EnumMemberProxy:
|
|
240
|
+
"""Wraps an enum member value to support ordered comparison within the same enum."""
|
|
241
|
+
|
|
242
|
+
__slots__ = ("_name", "_value", "_enum_cls", "_order")
|
|
243
|
+
|
|
244
|
+
def __init__(self, name: str, value: Any, enum_cls: type, order: int) -> None:
|
|
245
|
+
object.__setattr__(self, "_name", name)
|
|
246
|
+
object.__setattr__(self, "_value", value)
|
|
247
|
+
object.__setattr__(self, "_enum_cls", enum_cls)
|
|
248
|
+
object.__setattr__(self, "_order", order)
|
|
249
|
+
|
|
250
|
+
def __int__(self) -> int:
|
|
251
|
+
return int(self._value)
|
|
252
|
+
|
|
253
|
+
def __index__(self) -> int:
|
|
254
|
+
return int(self._value)
|
|
255
|
+
|
|
256
|
+
def __eq__(self, other: Any) -> bool:
|
|
257
|
+
if isinstance(other, _EnumMemberProxy):
|
|
258
|
+
return self._value == other._value
|
|
259
|
+
return self._value == other
|
|
260
|
+
|
|
261
|
+
def __ne__(self, other: Any) -> bool:
|
|
262
|
+
return not self.__eq__(other)
|
|
263
|
+
|
|
264
|
+
def __lt__(self, other: Any) -> bool:
|
|
265
|
+
if isinstance(other, _EnumMemberProxy):
|
|
266
|
+
if self._enum_cls is not other._enum_cls:
|
|
267
|
+
raise TypeError(f"'<' not supported between members of '{self._enum_cls.__name__}' and '{other._enum_cls.__name__}'")
|
|
268
|
+
return self._order < other._order
|
|
269
|
+
return NotImplemented
|
|
270
|
+
|
|
271
|
+
def __le__(self, other: Any) -> bool:
|
|
272
|
+
if isinstance(other, _EnumMemberProxy):
|
|
273
|
+
if self._enum_cls is not other._enum_cls:
|
|
274
|
+
raise TypeError(f"'<=' not supported between members of '{self._enum_cls.__name__}' and '{other._enum_cls.__name__}'")
|
|
275
|
+
return self._order <= other._order
|
|
276
|
+
return NotImplemented
|
|
277
|
+
|
|
278
|
+
def __gt__(self, other: Any) -> bool:
|
|
279
|
+
if isinstance(other, _EnumMemberProxy):
|
|
280
|
+
if self._enum_cls is not other._enum_cls:
|
|
281
|
+
raise TypeError(f"'>' not supported between members of '{self._enum_cls.__name__}' and '{other._enum_cls.__name__}'")
|
|
282
|
+
return self._order > other._order
|
|
283
|
+
return NotImplemented
|
|
284
|
+
|
|
285
|
+
def __ge__(self, other: Any) -> bool:
|
|
286
|
+
if isinstance(other, _EnumMemberProxy):
|
|
287
|
+
if self._enum_cls is not other._enum_cls:
|
|
288
|
+
raise TypeError(f"'>=' not supported between members of '{self._enum_cls.__name__}' and '{other._enum_cls.__name__}'")
|
|
289
|
+
return self._order >= other._order
|
|
290
|
+
return NotImplemented
|
|
291
|
+
|
|
292
|
+
def __hash__(self) -> int:
|
|
293
|
+
return hash((self._enum_cls, self._value))
|
|
294
|
+
|
|
295
|
+
def __repr__(self) -> str:
|
|
296
|
+
return f"{self._enum_cls.__name__}.{self._name}"
|
|
297
|
+
|
|
298
|
+
def __str__(self) -> str:
|
|
299
|
+
return self._name
|
|
300
|
+
|
|
301
|
+
def __format__(self, format_spec: str) -> str:
|
|
302
|
+
if not format_spec:
|
|
303
|
+
return self._name
|
|
304
|
+
if format_spec == "v":
|
|
305
|
+
return str(self._value)
|
|
306
|
+
if format_spec == "n":
|
|
307
|
+
return self._name
|
|
308
|
+
if format_spec == "r":
|
|
309
|
+
return repr(self)
|
|
310
|
+
if format_spec == "nv":
|
|
311
|
+
return f"{self._name}={self._value}"
|
|
312
|
+
return format(self._value, format_spec)
|
|
313
|
+
|
|
314
|
+
def __bool__(self) -> bool:
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
def __add__(self, other: Any) -> Any:
|
|
318
|
+
return self._value + (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
319
|
+
|
|
320
|
+
def __radd__(self, other: Any) -> Any:
|
|
321
|
+
return other + self._value
|
|
322
|
+
|
|
323
|
+
def __sub__(self, other: Any) -> Any:
|
|
324
|
+
return self._value - (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
325
|
+
|
|
326
|
+
def __rsub__(self, other: Any) -> Any:
|
|
327
|
+
return other - self._value
|
|
328
|
+
|
|
329
|
+
def __mul__(self, other: Any) -> Any:
|
|
330
|
+
return self._value * (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
331
|
+
|
|
332
|
+
def __rmul__(self, other: Any) -> Any:
|
|
333
|
+
return other * self._value
|
|
334
|
+
|
|
335
|
+
def __or__(self, other: Any) -> Any:
|
|
336
|
+
return self._value | (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
337
|
+
|
|
338
|
+
def __ror__(self, other: Any) -> Any:
|
|
339
|
+
return other | self._value
|
|
340
|
+
|
|
341
|
+
def __and__(self, other: Any) -> Any:
|
|
342
|
+
return self._value & (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
343
|
+
|
|
344
|
+
def __rand__(self, other: Any) -> Any:
|
|
345
|
+
return other & self._value
|
|
346
|
+
|
|
347
|
+
def __lshift__(self, other: Any) -> Any:
|
|
348
|
+
return self._value << (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
349
|
+
|
|
350
|
+
def __rlshift__(self, other: Any) -> Any:
|
|
351
|
+
return other << self._value
|
|
352
|
+
|
|
353
|
+
def __rshift__(self, other: Any) -> Any:
|
|
354
|
+
return self._value >> (other._value if isinstance(other, _EnumMemberProxy) else other)
|
|
355
|
+
|
|
356
|
+
def __rrshift__(self, other: Any) -> Any:
|
|
357
|
+
return other >> self._value
|
|
358
|
+
|
|
359
|
+
def __reduce__(self) -> tuple[Any, ...]:
|
|
360
|
+
return (_restore_member, (self._enum_cls, self._name))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _restore_member(cls: type, name: str) -> Any:
|
|
364
|
+
return getattr(cls, name)
|
|
365
|
+
|
|
217
366
|
|
|
218
367
|
class IotaEnum(metaclass=IotaEnumMeta):
|
|
219
368
|
|
|
@@ -319,3 +468,73 @@ class IotaBitFlags(metaclass=IotaEnumMeta):
|
|
|
319
468
|
|
|
320
469
|
class IotaStringEnum(IotaEnum):
|
|
321
470
|
_is_string_enum_base = True
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class FlagScope:
|
|
474
|
+
"""Context manager for scoped bitflag operations."""
|
|
475
|
+
|
|
476
|
+
def __init__(self, initial: BitFlag | int = 0) -> None:
|
|
477
|
+
self._flags = initial if isinstance(initial, BitFlag) else BitFlag(initial)
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def flags(self) -> BitFlag:
|
|
481
|
+
return self._flags
|
|
482
|
+
|
|
483
|
+
def grant(self, *flags: BitFlag | int) -> None:
|
|
484
|
+
for f in flags:
|
|
485
|
+
self._flags = self._flags | f
|
|
486
|
+
|
|
487
|
+
def revoke(self, *flags: BitFlag | int) -> None:
|
|
488
|
+
for f in flags:
|
|
489
|
+
fv = int(f)
|
|
490
|
+
self._flags = BitFlag(int(self._flags) & ~fv)
|
|
491
|
+
|
|
492
|
+
def has(self, flag: BitFlag | int) -> bool:
|
|
493
|
+
return self._flags.has(flag)
|
|
494
|
+
|
|
495
|
+
def __enter__(self) -> FlagScope:
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
def __exit__(self, *args: Any) -> None:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
def __repr__(self) -> str:
|
|
502
|
+
return f"FlagScope({self._flags})"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def iota_enum(cls: type | None = None, *, ordered: bool = False) -> Any:
|
|
506
|
+
"""Decorator to convert a plain class into an IotaEnum.
|
|
507
|
+
|
|
508
|
+
Usage:
|
|
509
|
+
@iota_enum
|
|
510
|
+
class Color:
|
|
511
|
+
Red = 0
|
|
512
|
+
Green = 1
|
|
513
|
+
Blue = 2
|
|
514
|
+
|
|
515
|
+
@iota_enum(ordered=True)
|
|
516
|
+
class Priority:
|
|
517
|
+
Low = 0
|
|
518
|
+
Medium = 1
|
|
519
|
+
High = 2
|
|
520
|
+
"""
|
|
521
|
+
def _wrap(klass: type) -> IotaEnumMeta:
|
|
522
|
+
ns = IotaNamespace()
|
|
523
|
+
if ordered:
|
|
524
|
+
ns["_ordered_"] = True
|
|
525
|
+
for key, val in vars(klass).items():
|
|
526
|
+
if key.startswith("_"):
|
|
527
|
+
continue
|
|
528
|
+
if callable(val) and not isinstance(val, (int, float, str)):
|
|
529
|
+
continue
|
|
530
|
+
ns[key] = val
|
|
531
|
+
ns._member_names.append(key)
|
|
532
|
+
return IotaEnumMeta(klass.__name__, (IotaEnum,), ns)
|
|
533
|
+
|
|
534
|
+
if cls is not None:
|
|
535
|
+
return _wrap(cls)
|
|
536
|
+
return _wrap
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
import builtins
|
|
540
|
+
builtins_enumerate = builtins.enumerate
|
|
@@ -149,6 +149,25 @@ class IotaSequence:
|
|
|
149
149
|
return _reduce(fn, iter(self), initial)
|
|
150
150
|
return _reduce(fn, iter(self))
|
|
151
151
|
|
|
152
|
+
def __add__(self, other: IotaSequence) -> _ConcatSequence:
|
|
153
|
+
return _ConcatSequence(self, other)
|
|
154
|
+
|
|
155
|
+
def __mul__(self, n: int) -> _RepeatSequence:
|
|
156
|
+
return _RepeatSequence(self, n)
|
|
157
|
+
|
|
158
|
+
def __rmul__(self, n: int) -> _RepeatSequence:
|
|
159
|
+
return _RepeatSequence(self, n)
|
|
160
|
+
|
|
161
|
+
def __or__(self, other: IotaSequence) -> _InterleaveSequence:
|
|
162
|
+
return _InterleaveSequence(self, other)
|
|
163
|
+
|
|
164
|
+
def __matmul__(self, other: IotaSequence) -> _ZipMapSequence:
|
|
165
|
+
return _ZipMapSequence(self, other)
|
|
166
|
+
|
|
167
|
+
async def __aiter__(self): # type: ignore[override]
|
|
168
|
+
for item in self:
|
|
169
|
+
yield item
|
|
170
|
+
|
|
152
171
|
def __repr__(self) -> str:
|
|
153
172
|
parts = []
|
|
154
173
|
if self._start != 0:
|
|
@@ -164,6 +183,125 @@ class IotaSequence:
|
|
|
164
183
|
return f"iota({', '.join(parts)})"
|
|
165
184
|
|
|
166
185
|
|
|
186
|
+
class _ConcatSequence(IotaSequence):
|
|
187
|
+
def __init__(self, a: IotaSequence, b: IotaSequence) -> None:
|
|
188
|
+
self._a = a
|
|
189
|
+
self._b = b
|
|
190
|
+
self._start = 0
|
|
191
|
+
self._stop = None if (a.infinite or b.infinite) else (len(a) + len(b))
|
|
192
|
+
self._step = 1
|
|
193
|
+
self._map = None
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def infinite(self) -> bool:
|
|
197
|
+
return self._a.infinite or self._b.infinite
|
|
198
|
+
|
|
199
|
+
def __iter__(self) -> Iterator[Any]:
|
|
200
|
+
yield from self._a
|
|
201
|
+
yield from self._b
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
return f"({self._a!r} + {self._b!r})"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _RepeatSequence(IotaSequence):
|
|
208
|
+
def __init__(self, seq: IotaSequence, n: int) -> None:
|
|
209
|
+
self._seq = seq
|
|
210
|
+
self._n = n
|
|
211
|
+
self._start = 0
|
|
212
|
+
self._stop = None if seq.infinite else len(seq) * n
|
|
213
|
+
self._step = 1
|
|
214
|
+
self._map = None
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def infinite(self) -> bool:
|
|
218
|
+
return self._seq.infinite
|
|
219
|
+
|
|
220
|
+
def __iter__(self) -> Iterator[Any]:
|
|
221
|
+
for _ in range(self._n):
|
|
222
|
+
yield from self._seq
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return f"({self._seq!r} * {self._n})"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _InterleaveSequence(IotaSequence):
|
|
229
|
+
def __init__(self, a: IotaSequence, b: IotaSequence) -> None:
|
|
230
|
+
self._a = a
|
|
231
|
+
self._b = b
|
|
232
|
+
self._start = 0
|
|
233
|
+
self._step = 1
|
|
234
|
+
self._map = None
|
|
235
|
+
if a.infinite or b.infinite:
|
|
236
|
+
self._stop = None
|
|
237
|
+
else:
|
|
238
|
+
self._stop = len(a) + len(b)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def infinite(self) -> bool:
|
|
242
|
+
return self._a.infinite or self._b.infinite
|
|
243
|
+
|
|
244
|
+
def __iter__(self) -> Iterator[Any]:
|
|
245
|
+
it_a = iter(self._a)
|
|
246
|
+
it_b = iter(self._b)
|
|
247
|
+
while True:
|
|
248
|
+
exhausted = 0
|
|
249
|
+
try:
|
|
250
|
+
yield next(it_a)
|
|
251
|
+
except StopIteration:
|
|
252
|
+
exhausted += 1
|
|
253
|
+
try:
|
|
254
|
+
yield next(it_b)
|
|
255
|
+
except StopIteration:
|
|
256
|
+
exhausted += 1
|
|
257
|
+
if exhausted == 2:
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
def __len__(self) -> int:
|
|
261
|
+
if self.infinite:
|
|
262
|
+
raise TypeError("infinite sequence has no len()")
|
|
263
|
+
return len(self._a) + len(self._b)
|
|
264
|
+
|
|
265
|
+
def __repr__(self) -> str:
|
|
266
|
+
return f"({self._a!r} | {self._b!r})"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class _ZipMapSequence(IotaSequence):
|
|
270
|
+
def __init__(self, a: IotaSequence, b: IotaSequence) -> None:
|
|
271
|
+
self._a = a
|
|
272
|
+
self._b = b
|
|
273
|
+
self._start = 0
|
|
274
|
+
self._step = 1
|
|
275
|
+
self._map = None
|
|
276
|
+
if a.infinite and b.infinite:
|
|
277
|
+
self._stop = None
|
|
278
|
+
elif a.infinite:
|
|
279
|
+
self._stop = len(b)
|
|
280
|
+
elif b.infinite:
|
|
281
|
+
self._stop = len(a)
|
|
282
|
+
else:
|
|
283
|
+
self._stop = min(len(a), len(b))
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def infinite(self) -> bool:
|
|
287
|
+
return self._a.infinite and self._b.infinite
|
|
288
|
+
|
|
289
|
+
def __iter__(self) -> Iterator[tuple[Any, Any]]:
|
|
290
|
+
yield from builtins_zip(self._a, self._b)
|
|
291
|
+
|
|
292
|
+
def __len__(self) -> int:
|
|
293
|
+
if self.infinite:
|
|
294
|
+
raise TypeError("infinite sequence has no len()")
|
|
295
|
+
if self._a.infinite:
|
|
296
|
+
return len(self._b)
|
|
297
|
+
if self._b.infinite:
|
|
298
|
+
return len(self._a)
|
|
299
|
+
return min(len(self._a), len(self._b))
|
|
300
|
+
|
|
301
|
+
def __repr__(self) -> str:
|
|
302
|
+
return f"({self._a!r} @ {self._b!r})"
|
|
303
|
+
|
|
304
|
+
|
|
167
305
|
import builtins
|
|
168
306
|
|
|
169
307
|
builtins_map = builtins.map
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pickle
|
|
3
|
+
import pytest
|
|
4
|
+
from pythoniota import (
|
|
5
|
+
IotaEnum, IotaBitFlags, IotaStringEnum,
|
|
6
|
+
FlagScope, iota_enum, iota, BitFlag,
|
|
7
|
+
)
|
|
8
|
+
from pythoniota.enum import _EnumMemberProxy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ── Enum callable: Color(0) → member ──
|
|
12
|
+
|
|
13
|
+
class TestEnumCallable:
|
|
14
|
+
def test_call_by_value(self):
|
|
15
|
+
class Color(IotaEnum):
|
|
16
|
+
Red = iota # noqa: F821
|
|
17
|
+
Green = iota # noqa: F821
|
|
18
|
+
|
|
19
|
+
assert Color(0) == 0
|
|
20
|
+
assert Color(1) == 1
|
|
21
|
+
|
|
22
|
+
def test_call_invalid(self):
|
|
23
|
+
class Color(IotaEnum):
|
|
24
|
+
Red = iota # noqa: F821
|
|
25
|
+
|
|
26
|
+
with pytest.raises(ValueError, match="not a valid"):
|
|
27
|
+
Color(99)
|
|
28
|
+
|
|
29
|
+
def test_call_bitflags(self):
|
|
30
|
+
class Perm(IotaBitFlags):
|
|
31
|
+
R = 1 << iota # noqa: F821
|
|
32
|
+
W = 1 << iota # noqa: F821
|
|
33
|
+
|
|
34
|
+
result = Perm(1)
|
|
35
|
+
assert isinstance(result, BitFlag)
|
|
36
|
+
assert result.value == 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Ordered comparison: < > <= >= with type safety ──
|
|
40
|
+
|
|
41
|
+
class TestOrderedEnum:
|
|
42
|
+
def test_less_than(self):
|
|
43
|
+
class Priority(IotaEnum):
|
|
44
|
+
_ordered_ = True
|
|
45
|
+
Low = iota # noqa: F821
|
|
46
|
+
Medium = iota # noqa: F821
|
|
47
|
+
High = iota # noqa: F821
|
|
48
|
+
|
|
49
|
+
assert Priority.Low < Priority.Medium
|
|
50
|
+
assert Priority.Medium < Priority.High
|
|
51
|
+
assert not (Priority.High < Priority.Low)
|
|
52
|
+
|
|
53
|
+
def test_greater_than(self):
|
|
54
|
+
class Priority(IotaEnum):
|
|
55
|
+
_ordered_ = True
|
|
56
|
+
Low = iota # noqa: F821
|
|
57
|
+
Medium = iota # noqa: F821
|
|
58
|
+
High = iota # noqa: F821
|
|
59
|
+
|
|
60
|
+
assert Priority.High > Priority.Low
|
|
61
|
+
assert not (Priority.Low > Priority.High)
|
|
62
|
+
|
|
63
|
+
def test_le_ge(self):
|
|
64
|
+
class P(IotaEnum):
|
|
65
|
+
_ordered_ = True
|
|
66
|
+
A = iota # noqa: F821
|
|
67
|
+
B = iota # noqa: F821
|
|
68
|
+
|
|
69
|
+
assert P.A <= P.A
|
|
70
|
+
assert P.A <= P.B
|
|
71
|
+
assert P.B >= P.A
|
|
72
|
+
assert P.B >= P.B
|
|
73
|
+
|
|
74
|
+
def test_cross_enum_comparison_raises(self):
|
|
75
|
+
class A(IotaEnum):
|
|
76
|
+
_ordered_ = True
|
|
77
|
+
X = iota # noqa: F821
|
|
78
|
+
|
|
79
|
+
class B(IotaEnum):
|
|
80
|
+
_ordered_ = True
|
|
81
|
+
X = iota # noqa: F821
|
|
82
|
+
|
|
83
|
+
with pytest.raises(TypeError, match="not supported between"):
|
|
84
|
+
A.X < B.X # noqa: B015
|
|
85
|
+
|
|
86
|
+
def test_ordered_equality(self):
|
|
87
|
+
class P(IotaEnum):
|
|
88
|
+
_ordered_ = True
|
|
89
|
+
A = iota # noqa: F821
|
|
90
|
+
B = iota # noqa: F821
|
|
91
|
+
|
|
92
|
+
assert P.A == P.A
|
|
93
|
+
assert P.A != P.B
|
|
94
|
+
|
|
95
|
+
def test_ordered_int_comparison(self):
|
|
96
|
+
class P(IotaEnum):
|
|
97
|
+
_ordered_ = True
|
|
98
|
+
A = iota # noqa: F821
|
|
99
|
+
|
|
100
|
+
assert P.A == 0
|
|
101
|
+
|
|
102
|
+
def test_ordered_hash(self):
|
|
103
|
+
class P(IotaEnum):
|
|
104
|
+
_ordered_ = True
|
|
105
|
+
A = iota # noqa: F821
|
|
106
|
+
B = iota # noqa: F821
|
|
107
|
+
|
|
108
|
+
s = {P.A, P.B, P.A}
|
|
109
|
+
assert len(s) == 2
|
|
110
|
+
|
|
111
|
+
def test_ordered_repr(self):
|
|
112
|
+
class Color(IotaEnum):
|
|
113
|
+
_ordered_ = True
|
|
114
|
+
Red = iota # noqa: F821
|
|
115
|
+
|
|
116
|
+
assert repr(Color.Red) == "Color.Red"
|
|
117
|
+
|
|
118
|
+
def test_ordered_str(self):
|
|
119
|
+
class Color(IotaEnum):
|
|
120
|
+
_ordered_ = True
|
|
121
|
+
Red = iota # noqa: F821
|
|
122
|
+
|
|
123
|
+
assert str(Color.Red) == "Red"
|
|
124
|
+
|
|
125
|
+
def test_ordered_arithmetic(self):
|
|
126
|
+
class P(IotaEnum):
|
|
127
|
+
_ordered_ = True
|
|
128
|
+
A = iota # noqa: F821
|
|
129
|
+
B = iota # noqa: F821
|
|
130
|
+
|
|
131
|
+
assert P.A + P.B == 1
|
|
132
|
+
assert P.B - P.A == 1
|
|
133
|
+
assert P.B * 3 == 3
|
|
134
|
+
assert 10 + P.A == 10
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── __format__ protocol ──
|
|
138
|
+
|
|
139
|
+
class TestFormat:
|
|
140
|
+
def test_format_default(self):
|
|
141
|
+
class Color(IotaEnum):
|
|
142
|
+
_ordered_ = True
|
|
143
|
+
Red = iota # noqa: F821
|
|
144
|
+
|
|
145
|
+
assert f"{Color.Red}" == "Red"
|
|
146
|
+
|
|
147
|
+
def test_format_v(self):
|
|
148
|
+
class Color(IotaEnum):
|
|
149
|
+
_ordered_ = True
|
|
150
|
+
Red = iota # noqa: F821
|
|
151
|
+
|
|
152
|
+
assert f"{Color.Red:v}" == "0"
|
|
153
|
+
|
|
154
|
+
def test_format_n(self):
|
|
155
|
+
class Color(IotaEnum):
|
|
156
|
+
_ordered_ = True
|
|
157
|
+
Red = iota # noqa: F821
|
|
158
|
+
|
|
159
|
+
assert f"{Color.Red:n}" == "Red"
|
|
160
|
+
|
|
161
|
+
def test_format_r(self):
|
|
162
|
+
class Color(IotaEnum):
|
|
163
|
+
_ordered_ = True
|
|
164
|
+
Red = iota # noqa: F821
|
|
165
|
+
|
|
166
|
+
assert f"{Color.Red:r}" == "Color.Red"
|
|
167
|
+
|
|
168
|
+
def test_format_nv(self):
|
|
169
|
+
class Color(IotaEnum):
|
|
170
|
+
_ordered_ = True
|
|
171
|
+
Red = iota # noqa: F821
|
|
172
|
+
|
|
173
|
+
assert f"{Color.Red:nv}" == "Red=0"
|
|
174
|
+
|
|
175
|
+
def test_format_numeric(self):
|
|
176
|
+
class Color(IotaEnum):
|
|
177
|
+
_ordered_ = True
|
|
178
|
+
Red = iota # noqa: F821
|
|
179
|
+
Blue = iota # noqa: F821
|
|
180
|
+
|
|
181
|
+
assert f"{Color.Blue:03d}" == "001"
|
|
182
|
+
|
|
183
|
+
def test_bitflag_format(self):
|
|
184
|
+
f = BitFlag(4, "Exec")
|
|
185
|
+
assert f"{f}" == "Exec"
|
|
186
|
+
assert f"{f!r}" == "BitFlag(Exec=4)"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ── Pickle support ──
|
|
190
|
+
|
|
191
|
+
class TestPickle:
|
|
192
|
+
def test_pickle_ordered_member(self):
|
|
193
|
+
class Color(IotaEnum):
|
|
194
|
+
_ordered_ = True
|
|
195
|
+
Red = iota # noqa: F821
|
|
196
|
+
Green = iota # noqa: F821
|
|
197
|
+
|
|
198
|
+
# _EnumMemberProxy.__reduce__ uses _restore_member which needs
|
|
199
|
+
# the class to be importable. For test purposes, verify __reduce__ returns valid tuple.
|
|
200
|
+
r = Color.Red.__reduce__()
|
|
201
|
+
assert len(r) == 2
|
|
202
|
+
fn, args = r
|
|
203
|
+
assert args[1] == "Red"
|
|
204
|
+
|
|
205
|
+
def test_pickle_bitflag(self):
|
|
206
|
+
f = BitFlag(4, "Exec")
|
|
207
|
+
data = pickle.dumps(f)
|
|
208
|
+
restored = pickle.loads(data)
|
|
209
|
+
assert restored.value == 4
|
|
210
|
+
assert restored.name == "Exec"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── Decorator API ──
|
|
214
|
+
|
|
215
|
+
class TestIotaEnumDecorator:
|
|
216
|
+
def test_basic_decorator(self):
|
|
217
|
+
@iota_enum
|
|
218
|
+
class Color:
|
|
219
|
+
Red = 0
|
|
220
|
+
Green = 1
|
|
221
|
+
Blue = 2
|
|
222
|
+
|
|
223
|
+
assert Color.Red == 0
|
|
224
|
+
assert Color.Green == 1
|
|
225
|
+
assert Color.Blue == 2
|
|
226
|
+
assert Color.names() == ["Red", "Green", "Blue"]
|
|
227
|
+
|
|
228
|
+
def test_decorator_with_ordered(self):
|
|
229
|
+
@iota_enum(ordered=True)
|
|
230
|
+
class Priority:
|
|
231
|
+
Low = 0
|
|
232
|
+
Medium = 1
|
|
233
|
+
High = 2
|
|
234
|
+
|
|
235
|
+
assert Priority.Low < Priority.Medium
|
|
236
|
+
assert Priority.High > Priority.Low
|
|
237
|
+
|
|
238
|
+
def test_decorator_iteration(self):
|
|
239
|
+
@iota_enum
|
|
240
|
+
class Dir:
|
|
241
|
+
N = 0
|
|
242
|
+
S = 1
|
|
243
|
+
E = 2
|
|
244
|
+
W = 3
|
|
245
|
+
|
|
246
|
+
assert len(Dir) == 4
|
|
247
|
+
assert list(Dir) == [("N", 0), ("S", 1), ("E", 2), ("W", 3)]
|
|
248
|
+
|
|
249
|
+
def test_decorator_to_dict(self):
|
|
250
|
+
@iota_enum
|
|
251
|
+
class X:
|
|
252
|
+
A = 10
|
|
253
|
+
B = 20
|
|
254
|
+
|
|
255
|
+
assert X.to_dict() == {"A": 10, "B": 20}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── FlagScope context manager ──
|
|
259
|
+
|
|
260
|
+
class TestFlagScope:
|
|
261
|
+
def test_basic_scope(self):
|
|
262
|
+
R = BitFlag(1, "R")
|
|
263
|
+
W = BitFlag(2, "W")
|
|
264
|
+
X = BitFlag(4, "X")
|
|
265
|
+
|
|
266
|
+
with FlagScope() as scope:
|
|
267
|
+
scope.grant(R, W)
|
|
268
|
+
assert scope.has(R)
|
|
269
|
+
assert scope.has(W)
|
|
270
|
+
assert not scope.has(X)
|
|
271
|
+
|
|
272
|
+
def test_revoke(self):
|
|
273
|
+
R = BitFlag(1, "R")
|
|
274
|
+
W = BitFlag(2, "W")
|
|
275
|
+
|
|
276
|
+
with FlagScope() as scope:
|
|
277
|
+
scope.grant(R, W)
|
|
278
|
+
scope.revoke(W)
|
|
279
|
+
assert scope.has(R)
|
|
280
|
+
assert not scope.has(W)
|
|
281
|
+
|
|
282
|
+
def test_initial_flags(self):
|
|
283
|
+
R = BitFlag(1, "R")
|
|
284
|
+
W = BitFlag(2, "W")
|
|
285
|
+
|
|
286
|
+
with FlagScope(R | W) as scope:
|
|
287
|
+
assert scope.has(R)
|
|
288
|
+
assert scope.has(W)
|
|
289
|
+
|
|
290
|
+
def test_repr(self):
|
|
291
|
+
with FlagScope() as scope:
|
|
292
|
+
assert "FlagScope" in repr(scope)
|
|
293
|
+
|
|
294
|
+
def test_with_bitflags_enum(self):
|
|
295
|
+
class Perm(IotaBitFlags):
|
|
296
|
+
R = 1 << iota # noqa: F821
|
|
297
|
+
W = 1 << iota # noqa: F821
|
|
298
|
+
X = 1 << iota # noqa: F821
|
|
299
|
+
|
|
300
|
+
with FlagScope() as scope:
|
|
301
|
+
scope.grant(Perm.R, Perm.W)
|
|
302
|
+
assert scope.has(Perm.R)
|
|
303
|
+
assert not scope.has(Perm.X)
|
|
304
|
+
scope.grant(Perm.X)
|
|
305
|
+
assert scope.has(Perm.X)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── Sequence algebra ──
|
|
309
|
+
|
|
310
|
+
class TestSequenceAlgebra:
|
|
311
|
+
def test_concat(self):
|
|
312
|
+
a = iota(3)
|
|
313
|
+
b = iota(3, 6)
|
|
314
|
+
result = list(a + b)
|
|
315
|
+
assert result == [0, 1, 2, 3, 4, 5]
|
|
316
|
+
|
|
317
|
+
def test_concat_repr(self):
|
|
318
|
+
r = repr(iota(3) + iota(3))
|
|
319
|
+
assert "+" in r
|
|
320
|
+
|
|
321
|
+
def test_repeat(self):
|
|
322
|
+
result = list(iota(3) * 2)
|
|
323
|
+
assert result == [0, 1, 2, 0, 1, 2]
|
|
324
|
+
|
|
325
|
+
def test_rmul(self):
|
|
326
|
+
result = list(2 * iota(3))
|
|
327
|
+
assert result == [0, 1, 2, 0, 1, 2]
|
|
328
|
+
|
|
329
|
+
def test_interleave(self):
|
|
330
|
+
a = iota(3)
|
|
331
|
+
b = iota(10, 13)
|
|
332
|
+
result = list(a | b)
|
|
333
|
+
assert result == [0, 10, 1, 11, 2, 12]
|
|
334
|
+
|
|
335
|
+
def test_interleave_unequal(self):
|
|
336
|
+
a = iota(2)
|
|
337
|
+
b = iota(10, 14)
|
|
338
|
+
result = list(a | b)
|
|
339
|
+
assert result == [0, 10, 1, 11, 12, 13]
|
|
340
|
+
|
|
341
|
+
def test_zipmap(self):
|
|
342
|
+
a = iota(3)
|
|
343
|
+
b = iota(10, 13)
|
|
344
|
+
result = list(a @ b)
|
|
345
|
+
assert result == [(0, 10), (1, 11), (2, 12)]
|
|
346
|
+
|
|
347
|
+
def test_chain_algebra(self):
|
|
348
|
+
result = list((iota(2) + iota(2)) * 2)
|
|
349
|
+
assert result == [0, 1, 0, 1, 0, 1, 0, 1]
|
|
350
|
+
|
|
351
|
+
def test_concat_len(self):
|
|
352
|
+
s = iota(3) + iota(4)
|
|
353
|
+
assert len(s) == 7
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ── Async iteration ──
|
|
357
|
+
|
|
358
|
+
class TestAsyncIter:
|
|
359
|
+
def test_async_iter(self):
|
|
360
|
+
async def collect():
|
|
361
|
+
result = []
|
|
362
|
+
async for x in iota(5):
|
|
363
|
+
result.append(x)
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
result = asyncio.run(collect())
|
|
367
|
+
assert result == [0, 1, 2, 3, 4]
|
|
368
|
+
|
|
369
|
+
def test_async_iter_mapped(self):
|
|
370
|
+
async def collect():
|
|
371
|
+
result = []
|
|
372
|
+
async for x in iota(4, map=lambda i: i * 10):
|
|
373
|
+
result.append(x)
|
|
374
|
+
return result
|
|
375
|
+
|
|
376
|
+
result = asyncio.run(collect())
|
|
377
|
+
assert result == [0, 10, 20, 30]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|