PythonIota 1.0.0__py3-none-any.whl → 1.2.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
@@ -1,15 +1,23 @@
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 (
5
+ IotaEnum, IotaBitFlags, IotaStringEnum,
6
+ FlagScope, iota_enum, _EnumMemberProxy,
7
+ )
5
8
  from pythoniota.sequence import IotaSequence as iota
9
+ from pythoniota import recipes
6
10
 
7
- __version__ = "1.0.0"
11
+ __version__ = "1.2.0"
8
12
  __all__ = [
9
13
  "Iota",
10
14
  "IotaEnum",
11
15
  "IotaBitFlags",
16
+ "IotaStringEnum",
12
17
  "iota",
13
18
  "BitFlag",
19
+ "FlagScope",
20
+ "iota_enum",
14
21
  "safe_eval",
22
+ "recipes",
15
23
  ]
pythoniota/_bitflag.py CHANGED
@@ -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
@@ -87,5 +101,19 @@ class BitFlag:
87
101
  def __str__(self) -> str:
88
102
  return self._name or str(self._value)
89
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
+
90
118
  def __iter__(self) -> Iterator[BitFlag]:
91
119
  return iter(self.decompose())
pythoniota/enum.py CHANGED
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ import pickle
3
5
  from collections import OrderedDict
4
- from typing import Any, Iterator
6
+ from contextlib import contextmanager
7
+ from typing import Any, Callable, Iterator
5
8
 
6
9
  from pythoniota._bitflag import BitFlag
7
10
 
@@ -124,7 +127,7 @@ class IotaNamespace(dict): # type: ignore[type-arg]
124
127
  if (
125
128
  not key.startswith("_")
126
129
  and key != "iota"
127
- and isinstance(value, (int, float, _IotaCounter))
130
+ and isinstance(value, (int, float, str, _IotaCounter))
128
131
  ):
129
132
  if key not in self._member_names:
130
133
  self._member_names.append(key)
@@ -144,25 +147,54 @@ class IotaEnumMeta(type):
144
147
  **kwargs: Any,
145
148
  ) -> IotaEnumMeta:
146
149
  member_names = namespace._member_names if isinstance(namespace, IotaNamespace) else []
147
- members: OrderedDict[str, int | float] = OrderedDict()
150
+ members: OrderedDict[str, Any] = OrderedDict()
148
151
  for mname in member_names:
149
152
  val = namespace[mname] if mname in namespace else 0
150
153
  members[mname] = int(val) if isinstance(val, _IotaCounter) else val
151
154
 
152
155
  cls = super().__new__(mcs, name, bases, dict(namespace))
153
156
  cls._members_ = members # type: ignore[attr-defined]
157
+ cls._aliases_: dict[str, str] = {} # type: ignore[attr-defined]
154
158
 
155
159
  is_bitflag = any(
156
160
  hasattr(b, "_is_bitflag_base") for b in bases
157
161
  )
158
- for mname, mval in members.items():
159
- if is_bitflag:
162
+ is_string_enum = any(
163
+ hasattr(b, "_is_string_enum_base") for b in bases
164
+ )
165
+
166
+ is_ordered = namespace.get("_ordered_", False) or any(
167
+ getattr(b, "_ordered_", False) for b in bases
168
+ )
169
+
170
+ if is_string_enum:
171
+ fmt = namespace.get("_format_", None)
172
+ for mname, mval in list(members.items()):
173
+ if isinstance(mval, _IotaCounter) or isinstance(mval, int):
174
+ if fmt:
175
+ members[mname] = fmt.format(name=mname, index=mval if isinstance(mval, int) else int(mval))
176
+ else:
177
+ members[mname] = mname
178
+ type.__setattr__(cls, mname, members[mname])
179
+ elif is_bitflag:
180
+ for mname, mval in members.items():
160
181
  flag = BitFlag(int(mval), mname)
161
182
  members[mname] = flag
162
183
  type.__setattr__(cls, mname, flag)
163
- else:
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)
189
+ else:
190
+ for mname, mval in members.items():
164
191
  type.__setattr__(cls, mname, mval)
165
192
 
193
+ doc_lines = [f"{name} enum members:"]
194
+ for mname, mval in members.items():
195
+ doc_lines.append(f" {mname} = {mval}")
196
+ cls.__doc__ = "\n".join(doc_lines)
197
+
166
198
  return cls
167
199
 
168
200
  def __iter__(cls) -> Iterator[tuple[str, int | float]]:
@@ -189,10 +221,148 @@ class IotaEnumMeta(type):
189
221
  raise AttributeError(f"Cannot delete enum member '{name}'")
190
222
  super().__delattr__(name)
191
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
+
192
231
  def __repr__(cls) -> str:
193
232
  items = ", ".join(f"{k}={v}" for k, v in cls._members_.items()) # type: ignore[attr-defined]
194
233
  return f"<{cls.__name__}: {items}>"
195
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
+
196
366
 
197
367
  class IotaEnum(metaclass=IotaEnumMeta):
198
368
 
@@ -201,20 +371,55 @@ class IotaEnum(metaclass=IotaEnumMeta):
201
371
  return list(cls._members_.keys()) # type: ignore[attr-defined]
202
372
 
203
373
  @classmethod
204
- def values(cls) -> list[int | float]:
374
+ def values(cls) -> list[Any]:
205
375
  return list(cls._members_.values()) # type: ignore[attr-defined]
206
376
 
207
377
  @classmethod
208
- def items(cls) -> list[tuple[str, int | float]]:
378
+ def items(cls) -> list[tuple[str, Any]]:
209
379
  return list(cls._members_.items()) # type: ignore[attr-defined]
210
380
 
211
381
  @classmethod
212
- def from_value(cls, value: int | float) -> str | None:
382
+ def from_value(cls, value: Any) -> str | None:
213
383
  for name, val in cls._members_.items(): # type: ignore[attr-defined]
214
384
  if val == value:
215
385
  return name
386
+ for alias, target in cls._aliases_.items(): # type: ignore[attr-defined]
387
+ if cls._members_[target] == value: # type: ignore[attr-defined]
388
+ return alias
216
389
  return None
217
390
 
391
+ @classmethod
392
+ def alias(cls, alias_name: str, target_name: str) -> None:
393
+ if target_name not in cls._members_: # type: ignore[attr-defined]
394
+ raise KeyError(f"'{target_name}' is not a member of {cls.__name__}")
395
+ cls._aliases_[alias_name] = target_name # type: ignore[attr-defined]
396
+ type.__setattr__(cls, alias_name, cls._members_[target_name]) # type: ignore[attr-defined]
397
+
398
+ @classmethod
399
+ def to_dict(cls) -> dict[str, Any]:
400
+ return dict(cls._members_) # type: ignore[attr-defined]
401
+
402
+ @classmethod
403
+ def to_json(cls, **kwargs: Any) -> str:
404
+ d = {}
405
+ 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
407
+ return json.dumps(d, **kwargs)
408
+
409
+ @classmethod
410
+ def from_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
411
+ result = {}
412
+ for name, value in data.items():
413
+ if name in cls._members_: # type: ignore[attr-defined]
414
+ result[name] = cls._members_[name] # type: ignore[attr-defined]
415
+ else:
416
+ raise KeyError(f"'{name}' is not a member of {cls.__name__}")
417
+ return result
418
+
419
+ @classmethod
420
+ def from_json(cls, s: str) -> dict[str, Any]:
421
+ return cls.from_dict(json.loads(s))
422
+
218
423
 
219
424
  class IotaBitFlags(metaclass=IotaEnumMeta):
220
425
  _is_bitflag_base = True
@@ -237,3 +442,99 @@ class IotaBitFlags(metaclass=IotaEnumMeta):
237
442
  if int(flag) == value:
238
443
  return name
239
444
  return None
445
+
446
+ @classmethod
447
+ def to_dict(cls) -> dict[str, int]:
448
+ return {k: int(v) for k, v in cls._members_.items()} # type: ignore[attr-defined]
449
+
450
+ @classmethod
451
+ def to_json(cls, **kwargs: Any) -> str:
452
+ return json.dumps(cls.to_dict(), **kwargs)
453
+
454
+ @classmethod
455
+ def from_dict(cls, data: dict[str, Any]) -> dict[str, BitFlag]:
456
+ result = {}
457
+ for name in data:
458
+ if name in cls._members_: # type: ignore[attr-defined]
459
+ result[name] = cls._members_[name] # type: ignore[attr-defined]
460
+ else:
461
+ raise KeyError(f"'{name}' is not a member of {cls.__name__}")
462
+ return result
463
+
464
+ @classmethod
465
+ def from_json(cls, s: str) -> dict[str, BitFlag]:
466
+ return cls.from_dict(json.loads(s))
467
+
468
+
469
+ class IotaStringEnum(IotaEnum):
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
pythoniota/recipes.py ADDED
@@ -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
pythoniota/sequence.py CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonIota
3
- Version: 1.0.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
@@ -0,0 +1,11 @@
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,,
@@ -1,10 +0,0 @@
1
- pythoniota/__init__.py,sha256=fO2WT2un6fpIg3h5qdB0fck4wP2zg48D3ej1dzyi1_0,353
2
- pythoniota/_bitflag.py,sha256=9gHyG-yFD-Z5Va9GclBaqKZzELfGx5m5aX5MUtpPQWk,2773
3
- pythoniota/_compat.py,sha256=kscV09rl3-1ccU5hm_dGj7EocKSLj0qUmRVUNOTmBwU,2075
4
- pythoniota/_safe_eval.py,sha256=FJgZxLp5eBDUg1MchVjkHSqnDOM9Jo6mmiupKZ2CvR4,2051
5
- pythoniota/enum.py,sha256=PzIJGNMDDoY1mxEhHIC8b8Nca_0_DJ0X7wV_l-3aLlE,7396
6
- pythoniota/sequence.py,sha256=7PtxrOcBAOAFtTwwSJwtI_h0vYROZyd3JNKfuF20PSA,5686
7
- pythoniota-1.0.0.dist-info/METADATA,sha256=ykPzwek9rW-7wL4V5JHMFpHntuPojw6nFGyWL5MXXF4,621
8
- pythoniota-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
- pythoniota-1.0.0.dist-info/top_level.txt,sha256=LoZ35nXIBPHY71-dvmTXyI_g9PBRt8hui5ubG4Huu8c,11
10
- pythoniota-1.0.0.dist-info/RECORD,,