PythonIota 1.0.0__tar.gz → 1.1.0__tar.gz

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