PythonIota 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pythoniota/__init__.py CHANGED
@@ -3,12 +3,12 @@ from pythoniota._bitflag import BitFlag
3
3
  from pythoniota._safe_eval import safe_eval
4
4
  from pythoniota.enum import (
5
5
  IotaEnum, IotaBitFlags, IotaStringEnum,
6
- FlagScope, iota_enum, _EnumMemberProxy,
6
+ FlagScope, iota_enum, unique, _EnumMemberProxy,
7
7
  )
8
8
  from pythoniota.sequence import IotaSequence as iota
9
9
  from pythoniota import recipes
10
10
 
11
- __version__ = "1.2.0"
11
+ __version__ = "1.3.0"
12
12
  __all__ = [
13
13
  "Iota",
14
14
  "IotaEnum",
@@ -18,6 +18,7 @@ __all__ = [
18
18
  "BitFlag",
19
19
  "FlagScope",
20
20
  "iota_enum",
21
+ "unique",
21
22
  "safe_eval",
22
23
  "recipes",
23
24
  ]
pythoniota/enum.py CHANGED
@@ -113,20 +113,32 @@ class IotaNamespace(dict): # type: ignore[type-arg]
113
113
  def __init__(self) -> None:
114
114
  super().__init__()
115
115
  self._counter = 0
116
+ self._step = 1
116
117
  self._member_names: list[str] = []
117
118
 
119
+ def _skip(self, n: int = 1) -> None:
120
+ """Advance the iota counter by `n` steps without defining a member (Go's blank `_`)."""
121
+ self._counter += n * self._step
122
+ return None
123
+
118
124
  def __getitem__(self, key: str) -> Any:
119
125
  if key == "iota":
120
126
  val = _IotaCounter(self._counter)
121
- self._counter += 1
127
+ self._counter += self._step
122
128
  return val
129
+ if key == "skip":
130
+ return self._skip
123
131
  return super().__getitem__(key)
124
132
 
125
133
  def __setitem__(self, key: str, value: Any) -> None:
134
+ if key == "_iota_start_":
135
+ self._counter = int(value)
136
+ elif key == "_iota_step_":
137
+ self._step = int(value)
126
138
  super().__setitem__(key, value)
127
139
  if (
128
140
  not key.startswith("_")
129
- and key != "iota"
141
+ and key not in ("iota", "skip")
130
142
  and isinstance(value, (int, float, str, _IotaCounter))
131
143
  ):
132
144
  if key not in self._member_names:
@@ -182,7 +194,7 @@ class IotaEnumMeta(type):
182
194
  members[mname] = flag
183
195
  type.__setattr__(cls, mname, flag)
184
196
  elif is_ordered:
185
- for order, (mname, mval) in builtins_enumerate(members.items()):
197
+ for order, (mname, mval) in enumerate(members.items()):
186
198
  proxy = _EnumMemberProxy(mname, mval, cls, order)
187
199
  members[mname] = proxy
188
200
  type.__setattr__(cls, mname, proxy)
@@ -388,6 +400,14 @@ class IotaEnum(metaclass=IotaEnumMeta):
388
400
  return alias
389
401
  return None
390
402
 
403
+ @classmethod
404
+ def has(cls, name: str) -> bool:
405
+ return name in cls._members_ # type: ignore[attr-defined]
406
+
407
+ @classmethod
408
+ def get(cls, name: str, default: Any = None) -> Any:
409
+ return cls._members_.get(name, default) # type: ignore[attr-defined]
410
+
391
411
  @classmethod
392
412
  def alias(cls, alias_name: str, target_name: str) -> None:
393
413
  if target_name not in cls._members_: # type: ignore[attr-defined]
@@ -397,13 +417,21 @@ class IotaEnum(metaclass=IotaEnumMeta):
397
417
 
398
418
  @classmethod
399
419
  def to_dict(cls) -> dict[str, Any]:
400
- return dict(cls._members_) # type: ignore[attr-defined]
420
+ d = {}
421
+ for k, v in cls._members_.items(): # type: ignore[attr-defined]
422
+ d[k] = v._value if isinstance(v, _EnumMemberProxy) else v
423
+ return d
401
424
 
402
425
  @classmethod
403
426
  def to_json(cls, **kwargs: Any) -> str:
404
427
  d = {}
405
428
  for k, v in cls._members_.items(): # type: ignore[attr-defined]
406
- d[k] = int(v) if isinstance(v, (BitFlag, float)) and not isinstance(v, str) else v
429
+ if isinstance(v, _EnumMemberProxy):
430
+ d[k] = v._value
431
+ elif isinstance(v, BitFlag):
432
+ d[k] = int(v)
433
+ else:
434
+ d[k] = v
407
435
  return json.dumps(d, **kwargs)
408
436
 
409
437
  @classmethod
@@ -443,6 +471,14 @@ class IotaBitFlags(metaclass=IotaEnumMeta):
443
471
  return name
444
472
  return None
445
473
 
474
+ @classmethod
475
+ def has(cls, name: str) -> bool:
476
+ return name in cls._members_ # type: ignore[attr-defined]
477
+
478
+ @classmethod
479
+ def get(cls, name: str, default: Any = None) -> Any:
480
+ return cls._members_.get(name, default) # type: ignore[attr-defined]
481
+
446
482
  @classmethod
447
483
  def to_dict(cls) -> dict[str, int]:
448
484
  return {k: int(v) for k, v in cls._members_.items()} # type: ignore[attr-defined]
@@ -536,5 +572,22 @@ def iota_enum(cls: type | None = None, *, ordered: bool = False) -> Any:
536
572
  return _wrap
537
573
 
538
574
 
539
- import builtins
540
- builtins_enumerate = builtins.enumerate
575
+ def _member_value(val: Any) -> Any:
576
+ if isinstance(val, _EnumMemberProxy):
577
+ return val._value
578
+ if isinstance(val, BitFlag):
579
+ return int(val)
580
+ return val
581
+
582
+
583
+ def unique(cls: type) -> type:
584
+ """Class decorator that rejects an enum with duplicate member values."""
585
+ seen: dict[Any, str] = {}
586
+ for name, val in cls._members_.items(): # type: ignore[attr-defined]
587
+ v = _member_value(val)
588
+ if v in seen:
589
+ raise ValueError(
590
+ f"duplicate value {v!r} in {cls.__name__}: {seen[v]} and {name}"
591
+ )
592
+ seen[v] = name
593
+ return cls
pythoniota/recipes.py CHANGED
@@ -234,3 +234,140 @@ def powers(base: int | float, n: int | None = None) -> IotaSequence:
234
234
  seq._step = 1
235
235
  seq._map = None
236
236
  return seq
237
+
238
+
239
+ def factorial(n: int | None = None) -> IotaSequence:
240
+ class FactorialSequence(IotaSequence):
241
+ def __iter__(self) -> Iterator[int]:
242
+ f = 1
243
+ i = 0
244
+ while self._stop is None or i < self._stop:
245
+ yield f
246
+ i += 1
247
+ f *= i
248
+
249
+ def __repr__(self) -> str:
250
+ return "factorial(infinite)" if self._stop is None else f"factorial({self._stop})"
251
+
252
+ seq = FactorialSequence.__new__(FactorialSequence)
253
+ seq._start = 0
254
+ seq._stop = n
255
+ seq._step = 1
256
+ seq._map = None
257
+ return seq
258
+
259
+
260
+ def catalan(n: int | None = None) -> IotaSequence:
261
+ class CatalanSequence(IotaSequence):
262
+ def __iter__(self) -> Iterator[int]:
263
+ c = 1
264
+ k = 0
265
+ while self._stop is None or k < self._stop:
266
+ yield c
267
+ c = c * 2 * (2 * k + 1) // (k + 2)
268
+ k += 1
269
+
270
+ def __repr__(self) -> str:
271
+ return "catalan(infinite)" if self._stop is None else f"catalan({self._stop})"
272
+
273
+ seq = CatalanSequence.__new__(CatalanSequence)
274
+ seq._start = 0
275
+ seq._stop = n
276
+ seq._step = 1
277
+ seq._map = None
278
+ return seq
279
+
280
+
281
+ def lucas(n: int | None = None) -> IotaSequence:
282
+ class LucasSequence(IotaSequence):
283
+ def __iter__(self) -> Iterator[int]:
284
+ a, b = 2, 1
285
+ if self._stop is None:
286
+ while True:
287
+ yield a
288
+ a, b = b, a + b
289
+ else:
290
+ count = 0
291
+ while count < self._stop:
292
+ yield a
293
+ a, b = b, a + b
294
+ count += 1
295
+
296
+ def __repr__(self) -> str:
297
+ return "lucas(infinite)" if self._stop is None else f"lucas({self._stop})"
298
+
299
+ seq = LucasSequence.__new__(LucasSequence)
300
+ seq._start = 0
301
+ seq._stop = n
302
+ seq._step = 1
303
+ seq._map = None
304
+ return seq
305
+
306
+
307
+ def harmonic(n: int | None = None) -> IotaSequence:
308
+ """Harmonic numbers H_k = sum(1/i for i in 1..k), yielded as floats starting at H_1."""
309
+
310
+ class HarmonicSequence(IotaSequence):
311
+ def __iter__(self) -> Iterator[float]:
312
+ total = 0.0
313
+ k = 1
314
+ while self._stop is None or k <= self._stop:
315
+ total += 1.0 / k
316
+ yield total
317
+ k += 1
318
+
319
+ def __repr__(self) -> str:
320
+ return "harmonic(infinite)" if self._stop is None else f"harmonic({self._stop})"
321
+
322
+ seq = HarmonicSequence.__new__(HarmonicSequence)
323
+ seq._start = 0
324
+ seq._stop = n
325
+ seq._step = 1
326
+ seq._map = None
327
+ return seq
328
+
329
+
330
+ def pascal_row(row: int) -> IotaSequence:
331
+ """The `row`-th row of Pascal's triangle: C(row, 0), C(row, 1), ..., C(row, row)."""
332
+ if row < 0:
333
+ raise ValueError("row must be non-negative")
334
+ return IotaSequence(row + 1, map=lambda k: math.comb(row, k))
335
+
336
+
337
+ def primes_sieve(limit: int) -> IotaSequence:
338
+ """All primes strictly less than `limit`, via the Sieve of Eratosthenes."""
339
+
340
+ class PrimesSieveSequence(IotaSequence):
341
+ def __init__(self, lim: int) -> None:
342
+ self._start = 0
343
+ self._stop = None
344
+ self._step = 1
345
+ self._map = None
346
+ self._limit = lim
347
+
348
+ @property
349
+ def infinite(self) -> bool:
350
+ return False
351
+
352
+ def __iter__(self) -> Iterator[int]:
353
+ lim = self._limit
354
+ if lim < 3:
355
+ return
356
+ sieve = bytearray([1]) * lim
357
+ sieve[0] = sieve[1] = 0
358
+ p = 2
359
+ while p * p < lim:
360
+ if sieve[p]:
361
+ sieve[p * p : lim : p] = bytearray(len(range(p * p, lim, p)))
362
+ p += 1
363
+ for i in range(2, lim):
364
+ if sieve[i]:
365
+ yield i
366
+
367
+ def __len__(self) -> int:
368
+ return sum(1 for _ in self)
369
+
370
+ def __repr__(self) -> str:
371
+ return f"primes_sieve({self._limit})"
372
+
373
+ return PrimesSieveSequence(limit)
pythoniota/sequence.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools
4
- import math
5
4
  from collections import deque
6
5
  from functools import reduce as _reduce
7
6
  from typing import Any, Callable, Iterator, TypeVar, overload
8
7
 
9
8
  T = TypeVar("T")
10
9
 
10
+ _MISSING: Any = object()
11
+
11
12
 
12
13
  class IotaSequence:
13
14
 
@@ -66,14 +67,15 @@ class IotaSequence:
66
67
  def __iter__(self) -> Iterator[Any]:
67
68
  it = self._raw_iter()
68
69
  if self._map is not None:
69
- return builtins_map(self._map, it)
70
+ return map(self._map, it)
70
71
  return it
71
72
 
72
73
  def __len__(self) -> int:
73
74
  if self.infinite:
74
75
  raise TypeError("infinite sequence has no len()")
75
76
  assert self._stop is not None
76
- n = math.ceil((self._stop - self._start) / self._step)
77
+ adjust = 1 if self._step > 0 else -1
78
+ n = (self._stop - self._start + self._step - adjust) // self._step
77
79
  return max(0, n)
78
80
 
79
81
  def __getitem__(self, index: int | slice) -> Any:
@@ -84,18 +86,42 @@ class IotaSequence:
84
86
  if self.infinite:
85
87
  raise IndexError("negative indexing not supported on infinite sequences")
86
88
  index = len(self) + index
89
+ if type(self) is IotaSequence:
90
+ if index < 0 or (not self.infinite and index >= self.__len__()):
91
+ raise IndexError("iota index out of range")
92
+ raw = self._start + index * self._step
93
+ return self._map(raw) if self._map is not None else raw
87
94
  return next(itertools.islice(iter(self), index, index + 1))
88
95
 
89
96
  def __contains__(self, value: Any) -> bool:
90
97
  if self.infinite:
91
98
  raise TypeError("cannot check membership on infinite sequence")
99
+ if (
100
+ type(self) is IotaSequence
101
+ and self._map is None
102
+ and isinstance(value, int)
103
+ and not isinstance(value, bool)
104
+ ):
105
+ assert self._stop is not None
106
+ if self._step > 0:
107
+ in_range = self._start <= value < self._stop
108
+ else:
109
+ in_range = self._stop < value <= self._start
110
+ return in_range and (value - self._start) % self._step == 0
92
111
  return value in iter(self)
93
112
 
94
113
  def __reversed__(self) -> Iterator[Any]:
95
114
  if self.infinite:
96
115
  raise TypeError("cannot reverse infinite sequence")
116
+ if type(self) is IotaSequence:
117
+ return self._reversed_arith()
97
118
  return reversed(list(self))
98
119
 
120
+ def _reversed_arith(self) -> Iterator[Any]:
121
+ for i in range(self.__len__() - 1, -1, -1):
122
+ raw = self._start + i * self._step
123
+ yield self._map(raw) if self._map is not None else raw
124
+
99
125
  def take(self, n: int) -> list[Any]:
100
126
  return list(itertools.islice(iter(self), n))
101
127
 
@@ -113,16 +139,16 @@ class IotaSequence:
113
139
  return seq
114
140
 
115
141
  def filter(self, fn: Callable[[Any], bool]) -> Iterator[Any]:
116
- return builtins_filter(fn, iter(self))
142
+ return filter(fn, iter(self))
117
143
 
118
144
  def accumulate(self, fn: Callable[[Any, Any], Any] | None = None) -> Iterator[Any]:
119
145
  return itertools.accumulate(iter(self), fn)
120
146
 
121
147
  def enumerate(self, start: int = 0) -> Iterator[tuple[int, Any]]:
122
- return builtins_enumerate(iter(self), start)
148
+ return enumerate(iter(self), start)
123
149
 
124
150
  def zip(self, *others: Any) -> Iterator[tuple[Any, ...]]:
125
- return builtins_zip(iter(self), *others)
151
+ return zip(iter(self), *others)
126
152
 
127
153
  def chunk(self, n: int) -> Iterator[list[Any]]:
128
154
  it = iter(self)
@@ -142,12 +168,145 @@ class IotaSequence:
142
168
  win.append(item)
143
169
  yield tuple(win)
144
170
 
145
- def reduce(self, fn: Callable[[Any, Any], Any], initial: Any = None) -> Any:
171
+ def reduce(self, fn: Callable[[Any, Any], Any], initial: Any = _MISSING) -> Any:
146
172
  if self.infinite:
147
173
  raise TypeError("cannot reduce infinite sequence")
148
- if initial is not None:
149
- return _reduce(fn, iter(self), initial)
150
- return _reduce(fn, iter(self))
174
+ if initial is _MISSING:
175
+ return _reduce(fn, iter(self))
176
+ return _reduce(fn, iter(self), initial)
177
+
178
+ # --- lazy combinators (return iterators) ---
179
+
180
+ def takewhile(self, pred: Callable[[Any], bool]) -> Iterator[Any]:
181
+ return itertools.takewhile(pred, iter(self))
182
+
183
+ def dropwhile(self, pred: Callable[[Any], bool]) -> Iterator[Any]:
184
+ return itertools.dropwhile(pred, iter(self))
185
+
186
+ def pairwise(self) -> Iterator[tuple[Any, Any]]:
187
+ return itertools.pairwise(iter(self))
188
+
189
+ def flatten(self) -> Iterator[Any]:
190
+ for item in self:
191
+ if isinstance(item, (str, bytes)):
192
+ yield item
193
+ else:
194
+ try:
195
+ inner = iter(item)
196
+ except TypeError:
197
+ yield item
198
+ else:
199
+ yield from inner
200
+
201
+ def distinct(self, key: Callable[[Any], Any] | None = None) -> Iterator[Any]:
202
+ seen: set[Any] = set()
203
+ for item in self:
204
+ k = key(item) if key is not None else item
205
+ if k not in seen:
206
+ seen.add(k)
207
+ yield item
208
+
209
+ # --- terminal operations (consume the sequence) ---
210
+
211
+ def _reject_infinite(self, op: str) -> None:
212
+ if self.infinite:
213
+ raise TypeError(f"cannot {op} an infinite sequence")
214
+
215
+ def to_list(self) -> list[Any]:
216
+ self._reject_infinite("materialize")
217
+ return list(self)
218
+
219
+ def to_tuple(self) -> tuple[Any, ...]:
220
+ self._reject_infinite("materialize")
221
+ return tuple(self)
222
+
223
+ def to_set(self) -> set[Any]:
224
+ self._reject_infinite("materialize")
225
+ return set(self)
226
+
227
+ def to_dict(
228
+ self,
229
+ key: Callable[[Any], Any] | None = None,
230
+ value: Callable[[Any], Any] | None = None,
231
+ ) -> dict[Any, Any]:
232
+ self._reject_infinite("materialize")
233
+ return {
234
+ (key(x) if key is not None else x): (value(x) if value is not None else x)
235
+ for x in self
236
+ }
237
+
238
+ def sum(self, start: Any = 0) -> Any:
239
+ self._reject_infinite("sum")
240
+ if type(self) is IotaSequence and self._map is None:
241
+ n = self.__len__()
242
+ if n == 0:
243
+ return start
244
+ last = self._start + (n - 1) * self._step
245
+ return start + n * (self._start + last) // 2
246
+ return sum(self, start)
247
+
248
+ def min(self, default: Any = _MISSING) -> Any:
249
+ self._reject_infinite("take min of")
250
+ if type(self) is IotaSequence and self._map is None:
251
+ n = self.__len__()
252
+ if n == 0:
253
+ if default is _MISSING:
254
+ raise ValueError("min() of an empty sequence")
255
+ return default
256
+ last = self._start + (n - 1) * self._step
257
+ return self._start if self._step > 0 else last
258
+ return min(self) if default is _MISSING else min(self, default=default)
259
+
260
+ def max(self, default: Any = _MISSING) -> Any:
261
+ self._reject_infinite("take max of")
262
+ if type(self) is IotaSequence and self._map is None:
263
+ n = self.__len__()
264
+ if n == 0:
265
+ if default is _MISSING:
266
+ raise ValueError("max() of an empty sequence")
267
+ return default
268
+ last = self._start + (n - 1) * self._step
269
+ return last if self._step > 0 else self._start
270
+ return max(self) if default is _MISSING else max(self, default=default)
271
+
272
+ def count(self) -> int:
273
+ self._reject_infinite("count")
274
+ if type(self) is IotaSequence:
275
+ return self.__len__()
276
+ return sum(1 for _ in self)
277
+
278
+ def first(self, default: Any = None) -> Any:
279
+ return next(iter(self), default)
280
+
281
+ def last(self, default: Any = None) -> Any:
282
+ self._reject_infinite("take last of")
283
+ if type(self) is IotaSequence:
284
+ n = self.__len__()
285
+ if n == 0:
286
+ return default
287
+ raw = self._start + (n - 1) * self._step
288
+ return self._map(raw) if self._map is not None else raw
289
+ result = default
290
+ for item in self:
291
+ result = item
292
+ return result
293
+
294
+ def nth(self, n: int, default: Any = None) -> Any:
295
+ if type(self) is IotaSequence and n >= 0:
296
+ if self.infinite or n < self.__len__():
297
+ raw = self._start + n * self._step
298
+ return self._map(raw) if self._map is not None else raw
299
+ return default
300
+ return next(itertools.islice(iter(self), n, n + 1), default)
301
+
302
+ def find(self, pred: Callable[[Any], bool], default: Any = None) -> Any:
303
+ return next((x for x in self if pred(x)), default)
304
+
305
+ def any(self, pred: Callable[[Any], bool] | None = None) -> bool:
306
+ return any(pred(x) if pred is not None else x for x in self)
307
+
308
+ def all(self, pred: Callable[[Any], bool] | None = None) -> bool:
309
+ return all(pred(x) if pred is not None else x for x in self)
151
310
 
152
311
  def __add__(self, other: IotaSequence) -> _ConcatSequence:
153
312
  return _ConcatSequence(self, other)
@@ -287,7 +446,7 @@ class _ZipMapSequence(IotaSequence):
287
446
  return self._a.infinite and self._b.infinite
288
447
 
289
448
  def __iter__(self) -> Iterator[tuple[Any, Any]]:
290
- yield from builtins_zip(self._a, self._b)
449
+ yield from zip(self._a, self._b)
291
450
 
292
451
  def __len__(self) -> int:
293
452
  if self.infinite:
@@ -300,11 +459,3 @@ class _ZipMapSequence(IotaSequence):
300
459
 
301
460
  def __repr__(self) -> str:
302
461
  return f"({self._a!r} @ {self._b!r})"
303
-
304
-
305
- import builtins
306
-
307
- builtins_map = builtins.map
308
- builtins_filter = builtins.filter
309
- builtins_enumerate = builtins.enumerate
310
- builtins_zip = builtins.zip
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: PythonIota
3
+ Version: 1.3.0
4
+ Summary: Go-style iota enumerations and flexible sequence generators for Python
5
+ Author: Equinox
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # PythonIota
19
+
20
+ Go-style `iota` enumerations and flexible, lazy sequence generators for Python — zero dependencies, fully typed.
21
+
22
+ ```bash
23
+ pip install PythonIota
24
+ ```
25
+
26
+ Requires Python 3.10+.
27
+
28
+ ---
29
+
30
+ ## Enums
31
+
32
+ ### Go-style `iota`
33
+
34
+ `iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.
35
+
36
+ ```python
37
+ from pythoniota import IotaEnum
38
+
39
+ class Color(IotaEnum):
40
+ Red = iota # 0
41
+ Green = iota # 1
42
+ Blue = iota # 2
43
+
44
+ Color.Green # 1
45
+ Color.names() # ['Red', 'Green', 'Blue']
46
+ Color.values() # [0, 1, 2]
47
+ list(Color) # [('Red', 0), ('Green', 1), ('Blue', 2)]
48
+ 0 in Color # True (by value)
49
+ 'Red' in Color # True (by name)
50
+ Color.from_value(2) # 'Blue'
51
+ ```
52
+
53
+ `iota` works inside expressions — the value is the current counter:
54
+
55
+ ```python
56
+ class Perm(IotaEnum):
57
+ Read = 1 << iota # 1
58
+ Write = 1 << iota # 2
59
+ Exec = 1 << iota # 4
60
+ All = Read | Write | Exec # 7
61
+
62
+ class Size(IotaEnum):
63
+ KB = 1 << (iota + 10) # 1024
64
+ MB = 1 << (iota + 10) # 2048
65
+ GB = 1 << (iota + 10) # 4096
66
+ ```
67
+
68
+ ### Skipping values & custom start/step
69
+
70
+ ```python
71
+ class Errno(IotaEnum):
72
+ EPERM = iota # 0
73
+ skip(2) # skip 1, 2
74
+ EBADF = iota # 3
75
+
76
+ class Port(IotaEnum):
77
+ _iota_start_ = 8000
78
+ _iota_step_ = 10
79
+ HTTP = iota # 8000
80
+ HTTPS = iota # 8010
81
+ ```
82
+
83
+ `_ = iota` also skips a single value (Go's blank identifier).
84
+
85
+ ### Immutability, aliases, serialization
86
+
87
+ ```python
88
+ Color.Red = 99 # AttributeError: cannot modify enum member
89
+ Color.alias('R', 'Red') # Color.R == 0
90
+ Color.to_dict() # {'Red': 0, 'Green': 1, 'Blue': 2}
91
+ Color.to_json() # '{"Red": 0, "Green": 1, "Blue": 2}'
92
+ Color.from_json(s) # -> dict of members
93
+ Color.has('Red') # True
94
+ Color.get('Nope', -1) # -1
95
+ ```
96
+
97
+ ### `@unique` — reject duplicate values
98
+
99
+ ```python
100
+ from pythoniota import unique
101
+
102
+ @unique
103
+ class Status(IotaEnum):
104
+ Active = iota
105
+ Inactive = iota
106
+ # raises ValueError if two members share a value
107
+ ```
108
+
109
+ ### Ordered enums
110
+
111
+ Add `_ordered_ = True` to make members comparable by declaration order:
112
+
113
+ ```python
114
+ class Priority(IotaEnum):
115
+ _ordered_ = True
116
+ Low = iota
117
+ Medium = iota
118
+ High = iota
119
+
120
+ Priority.Low < Priority.High # True
121
+ ```
122
+
123
+ ### Bit flags
124
+
125
+ ```python
126
+ from pythoniota import IotaBitFlags, FlagScope
127
+
128
+ class Access(IotaBitFlags):
129
+ Read = 1 << iota # 1
130
+ Write = 1 << iota # 2
131
+ Exec = 1 << iota # 4
132
+
133
+ flags = Access.Read | Access.Write
134
+ flags.has(Access.Read) # True
135
+ flags.has_all(Access.Read, Access.Exec) # False
136
+ flags.has_any(Access.Read, Access.Exec) # True
137
+ list(flags.decompose()) # [BitFlag(1), BitFlag(2)]
138
+
139
+ with FlagScope() as scope:
140
+ scope.grant(Access.Read, Access.Write)
141
+ scope.revoke(Access.Write)
142
+ scope.has(Access.Read) # True
143
+ ```
144
+
145
+ ### String enums
146
+
147
+ Members resolve to their own name, or a custom format:
148
+
149
+ ```python
150
+ from pythoniota import IotaStringEnum
151
+
152
+ class Color(IotaStringEnum):
153
+ Red = iota # 'Red'
154
+ Blue = iota # 'Blue'
155
+
156
+ class Code(IotaStringEnum):
157
+ _format_ = "{name}_{index:03d}"
158
+ OK = iota # 'OK_000'
159
+ Error = iota # 'Error_001'
160
+ ```
161
+
162
+ ### `@iota_enum` decorator
163
+
164
+ Turn a plain class into an enum:
165
+
166
+ ```python
167
+ from pythoniota import iota_enum
168
+
169
+ @iota_enum
170
+ class Color:
171
+ Red = 0
172
+ Green = 1
173
+ Blue = 2
174
+
175
+ @iota_enum(ordered=True)
176
+ class Priority:
177
+ Low = 0
178
+ High = 1
179
+ ```
180
+
181
+ ### `match` / `case`
182
+
183
+ Enum members match by value:
184
+
185
+ ```python
186
+ match color:
187
+ case Color.Red: ...
188
+ case Color.Blue: ...
189
+ case _: ...
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Sequences
195
+
196
+ `iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:
197
+
198
+ ```python
199
+ from pythoniota import iota
200
+
201
+ iota() # 0, 1, 2, ... (infinite)
202
+ iota(5) # 0, 1, 2, 3, 4
203
+ iota(2, 10, 2) # 2, 4, 6, 8
204
+ iota(5, map=lambda i: i*i) # 0, 1, 4, 9, 16
205
+
206
+ s = iota(10)
207
+ len(s) # 10 (O(1))
208
+ s[7] # 7 (O(1) arithmetic indexing)
209
+ s[2:5] # [2, 3, 4]
210
+ list(reversed(s)) # 9, 8, ... 0 (lazy)
211
+ 5 in s # True (O(1))
212
+ s.take(3) # [0, 1, 2]
213
+ ```
214
+
215
+ ### Operators
216
+
217
+ ```python
218
+ iota(3) + iota(3, 6) # concat: 0,1,2,3,4,5
219
+ iota(3) * 2 # repeat: 0,1,2,0,1,2
220
+ iota(3) | iota(3, 6) # interleave: 0,3,1,4,2,5
221
+ iota(3) @ iota(3, 6) # zip: (0,3),(1,4),(2,5)
222
+ ```
223
+
224
+ ### Lazy combinators
225
+
226
+ ```python
227
+ iota(10).filter(lambda x: x % 2 == 0) # 0,2,4,6,8
228
+ iota().map(lambda x: x*x) # 0,1,4,9,...
229
+ iota().takewhile(lambda x: x < 5) # 0,1,2,3,4
230
+ iota(10).dropwhile(lambda x: x < 7) # 7,8,9
231
+ iota(4).pairwise() # (0,1),(1,2),(2,3)
232
+ iota(5).window(3) # (0,1,2),(1,2,3),(2,3,4)
233
+ iota(7).chunk(3) # [0,1,2],[3,4,5],[6]
234
+ iota(6).map(lambda x: x % 3).distinct() # 0,1,2
235
+ seq.flatten() # flatten one level
236
+ iota().enumerate() .accumulate() .zip(other)
237
+ ```
238
+
239
+ ### Terminal operations
240
+
241
+ Reductions on plain arithmetic sequences are O(1); infinite sequences raise where they would not terminate.
242
+
243
+ ```python
244
+ iota(1, 101).sum() # 5050 (closed-form, O(1))
245
+ iota(3, 10).min() # 3
246
+ iota(3, 10).max() # 9
247
+ iota(0, 10, 2).count() # 5
248
+ iota(5).last() # 4
249
+ iota(10).nth(3) # 3
250
+ iota().first() # 0
251
+ iota().find(lambda x: x > 100) # 101
252
+ iota(1, 5).all(lambda x: x > 0) # True
253
+ iota(5).any(lambda x: x == 3) # True
254
+ iota(3).reduce(lambda a, b: a + b) # 3
255
+
256
+ iota(3).to_list() # [0, 1, 2]
257
+ iota(3).to_tuple() # (0, 1, 2)
258
+ iota(3).to_set() # {0, 1, 2}
259
+ iota(3).to_dict(value=lambda x: x*x) # {0: 0, 1: 1, 2: 4}
260
+ ```
261
+
262
+ ### Async iteration
263
+
264
+ ```python
265
+ async for x in iota(5):
266
+ ...
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Recipes
272
+
273
+ ```python
274
+ from pythoniota.recipes import (
275
+ fibonacci, lucas, factorial, catalan, harmonic,
276
+ triangle, powers, geometric, primes, primes_sieve,
277
+ pascal_row, collatz, repeat, cycle,
278
+ )
279
+
280
+ fibonacci(10).to_list() # 0,1,1,2,3,5,8,13,21,34
281
+ lucas(7).to_list() # 2,1,3,4,7,11,18
282
+ factorial(6).to_list() # 1,1,2,6,24,120
283
+ catalan(6).to_list() # 1,1,2,5,14,42
284
+ harmonic(4).to_list() # 1.0, 1.5, 1.833..., 2.083...
285
+ triangle(5).to_list() # 0,1,3,6,10
286
+ powers(2, 8).to_list() # 1,2,4,...,128
287
+ geometric(3, 2, 4).to_list() # 3,6,12,24
288
+ primes(5).to_list() # 2,3,5,7,11 (first n primes)
289
+ primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19 (< limit, Eratosthenes)
290
+ pascal_row(4).to_list() # 1,4,6,4,1
291
+ collatz(6).to_list() # 6,3,10,5,16,8,4,2,1
292
+ repeat('x', 3).to_list() # ['x','x','x']
293
+ cycle([1,2], 5).to_list() # 1,2,1,2,1
294
+ ```
295
+
296
+ Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.
297
+
298
+ ---
299
+
300
+ ## Safe expression evaluation
301
+
302
+ `safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):
303
+
304
+ ```python
305
+ from pythoniota import safe_eval
306
+
307
+ safe_eval("2 ** 10") # 1024
308
+ safe_eval("a * b + 1", {"a": 3, "b": 4}) # 13
309
+ ```
310
+
311
+ ---
312
+
313
+ ## License
314
+
315
+ MIT
@@ -0,0 +1,11 @@
1
+ pythoniota/__init__.py,sha256=kSSASpZFdDUTmw7TfRNvppXWpp_nVTxd88Jn8SV1bIk,546
2
+ pythoniota/_bitflag.py,sha256=EOOwudYcOECBN665TSFMtWIT9l5bT-hrdN0Gr_x_RQk,3692
3
+ pythoniota/_compat.py,sha256=kscV09rl3-1ccU5hm_dGj7EocKSLj0qUmRVUNOTmBwU,2075
4
+ pythoniota/_safe_eval.py,sha256=FJgZxLp5eBDUg1MchVjkHSqnDOM9Jo6mmiupKZ2CvR4,2051
5
+ pythoniota/enum.py,sha256=PQNk3rbY93C7AwNoD5LmdDNlr3gvppFMt15QedsTBUo,19846
6
+ pythoniota/recipes.py,sha256=W005nhwH4rqLfSXIsuFjl6ZjZvzHFEC9nrJH2_VibIA,10835
7
+ pythoniota/sequence.py,sha256=XMtbBIP-elaXkBiYOpSt0qPIYQOz_lGo2lHDh5z3V-A,15344
8
+ pythoniota-1.3.0.dist-info/METADATA,sha256=22aDmgyCECjd4KSDllYc1HK1NVTRkd3up-zaPyKFdo0,8084
9
+ pythoniota-1.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
10
+ pythoniota-1.3.0.dist-info/top_level.txt,sha256=LoZ35nXIBPHY71-dvmTXyI_g9PBRt8hui5ubG4Huu8c,11
11
+ pythoniota-1.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: PythonIota
3
- Version: 1.2.0
4
- Summary: Go-style iota enumerations and flexible sequence generators for Python
5
- Author: Equinox
6
- License: MIT
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3.10
9
- Classifier: Programming Language :: Python :: 3.11
10
- Classifier: Programming Language :: Python :: 3.12
11
- Classifier: Programming Language :: Python :: 3.13
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Typing :: Typed
15
- Requires-Python: >=3.10
16
- Description-Content-Type: text/markdown
@@ -1,11 +0,0 @@
1
- pythoniota/__init__.py,sha256=nQiMvhfaPQPcl6UyUmxv3aA39vqle0ARK4BwYMBhwlg,524
2
- pythoniota/_bitflag.py,sha256=EOOwudYcOECBN665TSFMtWIT9l5bT-hrdN0Gr_x_RQk,3692
3
- pythoniota/_compat.py,sha256=kscV09rl3-1ccU5hm_dGj7EocKSLj0qUmRVUNOTmBwU,2075
4
- pythoniota/_safe_eval.py,sha256=FJgZxLp5eBDUg1MchVjkHSqnDOM9Jo6mmiupKZ2CvR4,2051
5
- pythoniota/enum.py,sha256=Spwp_EosBA_ltMdHn3sRjJ-djF5_C8PJ-uKKbzTfd_4,18102
6
- pythoniota/recipes.py,sha256=5L4iAiUL2tRi6YG7XUQYTl2cUFY20gMP72BoEbf-Leo,6847
7
- pythoniota/sequence.py,sha256=Ov76UtMuhiVULdfYwb645eSeGuZZN-qEYhUGNFPiYe4,9559
8
- pythoniota-1.2.0.dist-info/METADATA,sha256=O87uw7fQS2qThEhLYbcipk1VgGffNoNtPlasV_iLYIw,621
9
- pythoniota-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
- pythoniota-1.2.0.dist-info/top_level.txt,sha256=LoZ35nXIBPHY71-dvmTXyI_g9PBRt8hui5ubG4Huu8c,11
11
- pythoniota-1.2.0.dist-info/RECORD,,