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.
Files changed (25) hide show
  1. {pythoniota-1.1.0 → pythoniota-1.2.0}/PKG-INFO +1 -1
  2. {pythoniota-1.1.0 → pythoniota-1.2.0}/pyproject.toml +1 -1
  3. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/PKG-INFO +1 -1
  4. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/SOURCES.txt +1 -0
  5. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/__init__.py +7 -2
  6. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_bitflag.py +14 -0
  7. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/enum.py +220 -1
  8. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/sequence.py +138 -0
  9. pythoniota-1.2.0/tests/test_advanced.py +377 -0
  10. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_integration.py +1 -1
  11. {pythoniota-1.1.0 → pythoniota-1.2.0}/setup.cfg +0 -0
  12. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
  13. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/PythonIota.egg-info/top_level.txt +0 -0
  14. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_compat.py +0 -0
  15. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/_safe_eval.py +0 -0
  16. {pythoniota-1.1.0 → pythoniota-1.2.0}/src/pythoniota/recipes.py +0 -0
  17. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_bitflags.py +0 -0
  18. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_compat.py +0 -0
  19. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_enum.py +0 -0
  20. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_enum_enhanced.py +0 -0
  21. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_recipes.py +0 -0
  22. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_safe_eval.py +0 -0
  23. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_sequence.py +0 -0
  24. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_serialization.py +0 -0
  25. {pythoniota-1.1.0 → pythoniota-1.2.0}/tests/test_string_enum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonIota
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Go-style iota enumerations and flexible sequence generators for Python
5
5
  Author: Equinox
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PythonIota"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Go-style iota enumerations and flexible sequence generators for Python"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonIota
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Go-style iota enumerations and flexible sequence generators for Python
5
5
  Author: Equinox
6
6
  License: MIT
@@ -10,6 +10,7 @@ src/pythoniota/_safe_eval.py
10
10
  src/pythoniota/enum.py
11
11
  src/pythoniota/recipes.py
12
12
  src/pythoniota/sequence.py
13
+ tests/test_advanced.py
13
14
  tests/test_bitflags.py
14
15
  tests/test_compat.py
15
16
  tests/test_enum.py
@@ -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 IotaEnum, IotaBitFlags, IotaStringEnum
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.1.0"
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 typing import Any, Iterator
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]
@@ -63,4 +63,4 @@ class TestIntegration:
63
63
  def test_version_exists(self):
64
64
  import pythoniota
65
65
  assert hasattr(pythoniota, "__version__")
66
- assert pythoniota.__version__ == "1.1.0"
66
+ assert pythoniota.__version__ == "1.2.0"
File without changes