PythonIota 1.2.0__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythoniota-1.7.0/PKG-INFO +368 -0
- pythoniota-1.7.0/README.md +351 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/pyproject.toml +2 -2
- pythoniota-1.7.0/src/PythonIota.egg-info/PKG-INFO +368 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/PythonIota.egg-info/SOURCES.txt +16 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/__init__.py +7 -2
- pythoniota-1.7.0/src/pythoniota/cli/__init__.py +27 -0
- pythoniota-1.7.0/src/pythoniota/cli/_argument.py +70 -0
- pythoniota-1.7.0/src/pythoniota/cli/_errors.py +9 -0
- pythoniota-1.7.0/src/pythoniota/cli/_formatter.py +93 -0
- pythoniota-1.7.0/src/pythoniota/cli/_namespace.py +17 -0
- pythoniota-1.7.0/src/pythoniota/cli/_parser.py +321 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/enum.py +60 -7
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/recipes.py +137 -0
- pythoniota-1.7.0/src/pythoniota/regex/__init__.py +66 -0
- pythoniota-1.7.0/src/pythoniota/regex/_compiler.py +177 -0
- pythoniota-1.7.0/src/pythoniota/regex/_parser.py +463 -0
- pythoniota-1.7.0/src/pythoniota/regex/_pattern.py +210 -0
- pythoniota-1.7.0/src/pythoniota/regex/_vm.py +123 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/sequence.py +170 -19
- pythoniota-1.7.0/tests/test_cli.py +244 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_integration.py +1 -1
- pythoniota-1.7.0/tests/test_new_features.py +243 -0
- pythoniota-1.7.0/tests/test_regex.py +145 -0
- pythoniota-1.7.0/tests/test_regex_extended.py +200 -0
- pythoniota-1.2.0/PKG-INFO +0 -16
- pythoniota-1.2.0/src/PythonIota.egg-info/PKG-INFO +0 -16
- {pythoniota-1.2.0 → pythoniota-1.7.0}/setup.cfg +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/PythonIota.egg-info/top_level.txt +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/_bitflag.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/_compat.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/src/pythoniota/_safe_eval.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_advanced.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_bitflags.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_compat.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_enum.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_enum_enhanced.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_recipes.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_safe_eval.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_sequence.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_serialization.py +0 -0
- {pythoniota-1.2.0 → pythoniota-1.7.0}/tests/test_string_enum.py +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PythonIota
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: A zero-dependency Python toolkit: Go-style iota enums, sequence generators, a from-scratch regex engine, and an argparse-style CLI parser
|
|
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
|
+
A **zero-dependency Python toolkit** of from-scratch building blocks — Go-style
|
|
21
|
+
`iota` enumerations, lazy sequence generators, and a linear-time regex engine.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install PythonIota
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Python 3.10+.
|
|
28
|
+
|
|
29
|
+
**What's inside**
|
|
30
|
+
|
|
31
|
+
- **Enums** — Go-style `iota`, bit flags, string enums (`pythoniota.IotaEnum`, ...)
|
|
32
|
+
- **Sequences** — lazy, chainable generators (`pythoniota.iota`)
|
|
33
|
+
- **Regex** — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (`pythoniota.regex`)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Enums
|
|
38
|
+
|
|
39
|
+
### Go-style `iota`
|
|
40
|
+
|
|
41
|
+
`iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pythoniota import IotaEnum
|
|
45
|
+
|
|
46
|
+
class Color(IotaEnum):
|
|
47
|
+
Red = iota # 0
|
|
48
|
+
Green = iota # 1
|
|
49
|
+
Blue = iota # 2
|
|
50
|
+
|
|
51
|
+
Color.Green # 1
|
|
52
|
+
Color.names() # ['Red', 'Green', 'Blue']
|
|
53
|
+
Color.values() # [0, 1, 2]
|
|
54
|
+
list(Color) # [('Red', 0), ('Green', 1), ('Blue', 2)]
|
|
55
|
+
0 in Color # True (by value)
|
|
56
|
+
'Red' in Color # True (by name)
|
|
57
|
+
Color.from_value(2) # 'Blue'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`iota` works inside expressions — the value is the current counter:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class Perm(IotaEnum):
|
|
64
|
+
Read = 1 << iota # 1
|
|
65
|
+
Write = 1 << iota # 2
|
|
66
|
+
Exec = 1 << iota # 4
|
|
67
|
+
All = Read | Write | Exec # 7
|
|
68
|
+
|
|
69
|
+
class Size(IotaEnum):
|
|
70
|
+
KB = 1 << (iota + 10) # 1024
|
|
71
|
+
MB = 1 << (iota + 10) # 2048
|
|
72
|
+
GB = 1 << (iota + 10) # 4096
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Skipping values & custom start/step
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
class Errno(IotaEnum):
|
|
79
|
+
EPERM = iota # 0
|
|
80
|
+
skip(2) # skip 1, 2
|
|
81
|
+
EBADF = iota # 3
|
|
82
|
+
|
|
83
|
+
class Port(IotaEnum):
|
|
84
|
+
_iota_start_ = 8000
|
|
85
|
+
_iota_step_ = 10
|
|
86
|
+
HTTP = iota # 8000
|
|
87
|
+
HTTPS = iota # 8010
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`_ = iota` also skips a single value (Go's blank identifier).
|
|
91
|
+
|
|
92
|
+
### Immutability, aliases, serialization
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
Color.Red = 99 # AttributeError: cannot modify enum member
|
|
96
|
+
Color.alias('R', 'Red') # Color.R == 0
|
|
97
|
+
Color.to_dict() # {'Red': 0, 'Green': 1, 'Blue': 2}
|
|
98
|
+
Color.to_json() # '{"Red": 0, "Green": 1, "Blue": 2}'
|
|
99
|
+
Color.from_json(s) # -> dict of members
|
|
100
|
+
Color.has('Red') # True
|
|
101
|
+
Color.get('Nope', -1) # -1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `@unique` — reject duplicate values
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from pythoniota import unique
|
|
108
|
+
|
|
109
|
+
@unique
|
|
110
|
+
class Status(IotaEnum):
|
|
111
|
+
Active = iota
|
|
112
|
+
Inactive = iota
|
|
113
|
+
# raises ValueError if two members share a value
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Ordered enums
|
|
117
|
+
|
|
118
|
+
Add `_ordered_ = True` to make members comparable by declaration order:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
class Priority(IotaEnum):
|
|
122
|
+
_ordered_ = True
|
|
123
|
+
Low = iota
|
|
124
|
+
Medium = iota
|
|
125
|
+
High = iota
|
|
126
|
+
|
|
127
|
+
Priority.Low < Priority.High # True
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Bit flags
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pythoniota import IotaBitFlags, FlagScope
|
|
134
|
+
|
|
135
|
+
class Access(IotaBitFlags):
|
|
136
|
+
Read = 1 << iota # 1
|
|
137
|
+
Write = 1 << iota # 2
|
|
138
|
+
Exec = 1 << iota # 4
|
|
139
|
+
|
|
140
|
+
flags = Access.Read | Access.Write
|
|
141
|
+
flags.has(Access.Read) # True
|
|
142
|
+
flags.has_all(Access.Read, Access.Exec) # False
|
|
143
|
+
flags.has_any(Access.Read, Access.Exec) # True
|
|
144
|
+
list(flags.decompose()) # [BitFlag(1), BitFlag(2)]
|
|
145
|
+
|
|
146
|
+
with FlagScope() as scope:
|
|
147
|
+
scope.grant(Access.Read, Access.Write)
|
|
148
|
+
scope.revoke(Access.Write)
|
|
149
|
+
scope.has(Access.Read) # True
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### String enums
|
|
153
|
+
|
|
154
|
+
Members resolve to their own name, or a custom format:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pythoniota import IotaStringEnum
|
|
158
|
+
|
|
159
|
+
class Color(IotaStringEnum):
|
|
160
|
+
Red = iota # 'Red'
|
|
161
|
+
Blue = iota # 'Blue'
|
|
162
|
+
|
|
163
|
+
class Code(IotaStringEnum):
|
|
164
|
+
_format_ = "{name}_{index:03d}"
|
|
165
|
+
OK = iota # 'OK_000'
|
|
166
|
+
Error = iota # 'Error_001'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `@iota_enum` decorator
|
|
170
|
+
|
|
171
|
+
Turn a plain class into an enum:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from pythoniota import iota_enum
|
|
175
|
+
|
|
176
|
+
@iota_enum
|
|
177
|
+
class Color:
|
|
178
|
+
Red = 0
|
|
179
|
+
Green = 1
|
|
180
|
+
Blue = 2
|
|
181
|
+
|
|
182
|
+
@iota_enum(ordered=True)
|
|
183
|
+
class Priority:
|
|
184
|
+
Low = 0
|
|
185
|
+
High = 1
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `match` / `case`
|
|
189
|
+
|
|
190
|
+
Enum members match by value:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
match color:
|
|
194
|
+
case Color.Red: ...
|
|
195
|
+
case Color.Blue: ...
|
|
196
|
+
case _: ...
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Sequences
|
|
202
|
+
|
|
203
|
+
`iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from pythoniota import iota
|
|
207
|
+
|
|
208
|
+
iota() # 0, 1, 2, ... (infinite)
|
|
209
|
+
iota(5) # 0, 1, 2, 3, 4
|
|
210
|
+
iota(2, 10, 2) # 2, 4, 6, 8
|
|
211
|
+
iota(5, map=lambda i: i*i) # 0, 1, 4, 9, 16
|
|
212
|
+
|
|
213
|
+
s = iota(10)
|
|
214
|
+
len(s) # 10 (O(1))
|
|
215
|
+
s[7] # 7 (O(1) arithmetic indexing)
|
|
216
|
+
s[2:5] # [2, 3, 4]
|
|
217
|
+
list(reversed(s)) # 9, 8, ... 0 (lazy)
|
|
218
|
+
5 in s # True (O(1))
|
|
219
|
+
s.take(3) # [0, 1, 2]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Operators
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
iota(3) + iota(3, 6) # concat: 0,1,2,3,4,5
|
|
226
|
+
iota(3) * 2 # repeat: 0,1,2,0,1,2
|
|
227
|
+
iota(3) | iota(3, 6) # interleave: 0,3,1,4,2,5
|
|
228
|
+
iota(3) @ iota(3, 6) # zip: (0,3),(1,4),(2,5)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Lazy combinators
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
iota(10).filter(lambda x: x % 2 == 0) # 0,2,4,6,8
|
|
235
|
+
iota().map(lambda x: x*x) # 0,1,4,9,...
|
|
236
|
+
iota().takewhile(lambda x: x < 5) # 0,1,2,3,4
|
|
237
|
+
iota(10).dropwhile(lambda x: x < 7) # 7,8,9
|
|
238
|
+
iota(4).pairwise() # (0,1),(1,2),(2,3)
|
|
239
|
+
iota(5).window(3) # (0,1,2),(1,2,3),(2,3,4)
|
|
240
|
+
iota(7).chunk(3) # [0,1,2],[3,4,5],[6]
|
|
241
|
+
iota(6).map(lambda x: x % 3).distinct() # 0,1,2
|
|
242
|
+
seq.flatten() # flatten one level
|
|
243
|
+
iota().enumerate() .accumulate() .zip(other)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Terminal operations
|
|
247
|
+
|
|
248
|
+
Reductions on plain arithmetic sequences are O(1); infinite sequences raise where they would not terminate.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
iota(1, 101).sum() # 5050 (closed-form, O(1))
|
|
252
|
+
iota(3, 10).min() # 3
|
|
253
|
+
iota(3, 10).max() # 9
|
|
254
|
+
iota(0, 10, 2).count() # 5
|
|
255
|
+
iota(5).last() # 4
|
|
256
|
+
iota(10).nth(3) # 3
|
|
257
|
+
iota().first() # 0
|
|
258
|
+
iota().find(lambda x: x > 100) # 101
|
|
259
|
+
iota(1, 5).all(lambda x: x > 0) # True
|
|
260
|
+
iota(5).any(lambda x: x == 3) # True
|
|
261
|
+
iota(3).reduce(lambda a, b: a + b) # 3
|
|
262
|
+
|
|
263
|
+
iota(3).to_list() # [0, 1, 2]
|
|
264
|
+
iota(3).to_tuple() # (0, 1, 2)
|
|
265
|
+
iota(3).to_set() # {0, 1, 2}
|
|
266
|
+
iota(3).to_dict(value=lambda x: x*x) # {0: 0, 1: 1, 2: 4}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Async iteration
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
async for x in iota(5):
|
|
273
|
+
...
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Recipes
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from pythoniota.recipes import (
|
|
282
|
+
fibonacci, lucas, factorial, catalan, harmonic,
|
|
283
|
+
triangle, powers, geometric, primes, primes_sieve,
|
|
284
|
+
pascal_row, collatz, repeat, cycle,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
fibonacci(10).to_list() # 0,1,1,2,3,5,8,13,21,34
|
|
288
|
+
lucas(7).to_list() # 2,1,3,4,7,11,18
|
|
289
|
+
factorial(6).to_list() # 1,1,2,6,24,120
|
|
290
|
+
catalan(6).to_list() # 1,1,2,5,14,42
|
|
291
|
+
harmonic(4).to_list() # 1.0, 1.5, 1.833..., 2.083...
|
|
292
|
+
triangle(5).to_list() # 0,1,3,6,10
|
|
293
|
+
powers(2, 8).to_list() # 1,2,4,...,128
|
|
294
|
+
geometric(3, 2, 4).to_list() # 3,6,12,24
|
|
295
|
+
primes(5).to_list() # 2,3,5,7,11 (first n primes)
|
|
296
|
+
primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19 (< limit, Eratosthenes)
|
|
297
|
+
pascal_row(4).to_list() # 1,4,6,4,1
|
|
298
|
+
collatz(6).to_list() # 6,3,10,5,16,8,4,2,1
|
|
299
|
+
repeat('x', 3).to_list() # ['x','x','x']
|
|
300
|
+
cycle([1,2], 5).to_list() # 1,2,1,2,1
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Safe expression evaluation
|
|
308
|
+
|
|
309
|
+
`safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from pythoniota import safe_eval
|
|
313
|
+
|
|
314
|
+
safe_eval("2 ** 10") # 1024
|
|
315
|
+
safe_eval("a * b + 1", {"a": 3, "b": 4}) # 13
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Regex
|
|
321
|
+
|
|
322
|
+
A from-scratch regular-expression engine. Unlike Python's backtracking `re`, it
|
|
323
|
+
compiles the pattern to a tiny bytecode and runs it with a **Pike VM** (a
|
|
324
|
+
thread-list NFA simulation), so matching is linear in `len(text) × len(program)`
|
|
325
|
+
— pathological patterns like `(a+)+$` finish instantly instead of hanging.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from pythoniota import regex
|
|
329
|
+
|
|
330
|
+
m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
|
|
331
|
+
m.groups() # ('2026', '08', '02')
|
|
332
|
+
|
|
333
|
+
regex.findall(r"\w+", "hello world") # ['hello', 'world']
|
|
334
|
+
regex.sub(r"\s+", "_", "a b c") # 'a_b_c'
|
|
335
|
+
regex.split(r"[,;]\s*", "a, b; c,d") # ['a', 'b', 'c', 'd']
|
|
336
|
+
|
|
337
|
+
named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
|
|
338
|
+
named.groupdict() # {'user': 'user', 'host': 'host'}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Supported: literals, `.`, `* + ?` and their lazy forms `*? +? ??`, alternation
|
|
342
|
+
`|`, groups `(...)` / non-capturing `(?:...)` / named `(?P<name>...)`, lookahead
|
|
343
|
+
`(?=...)` / `(?!...)`, character classes `[...]`, counted repeats `{m,n}`,
|
|
344
|
+
shorthands `\d \w \s` (and `\D \W \S`), word boundaries `\b \B`, and anchors
|
|
345
|
+
`^ $ \A \Z`. Flags: `IGNORECASE`, `MULTILINE`, `DOTALL`, `VERBOSE` (also inline
|
|
346
|
+
`(?imsx)`). API mirrors `re`: `compile`, `match`, `fullmatch`, `search`,
|
|
347
|
+
`findall`, `finditer`, `sub`, `split`, `escape`, with `Match.group` / `groups` /
|
|
348
|
+
`span` / `groupdict`.
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE) # ['AbCdEf']
|
|
352
|
+
regex.findall(r"\d+(?=px)", "10px 20em 30px") # ['10', '30'] (lookahead)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Backreferences (`\1`) are intentionally *not* supported: they can't be matched
|
|
356
|
+
by a finite automaton in linear time — that's the price of the no-backtracking
|
|
357
|
+
guarantee (the same trade-off Go's `regexp` and RE2 make).
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# no ReDoS: this returns in microseconds, where re takes ~13s at length 28
|
|
361
|
+
regex.search(r"(a+)+$", "a" * 40 + "!") # None, instantly
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## License
|
|
367
|
+
|
|
368
|
+
MIT
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# PythonIota
|
|
2
|
+
|
|
3
|
+
A **zero-dependency Python toolkit** of from-scratch building blocks — Go-style
|
|
4
|
+
`iota` enumerations, lazy sequence generators, and a linear-time regex engine.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install PythonIota
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires Python 3.10+.
|
|
11
|
+
|
|
12
|
+
**What's inside**
|
|
13
|
+
|
|
14
|
+
- **Enums** — Go-style `iota`, bit flags, string enums (`pythoniota.IotaEnum`, ...)
|
|
15
|
+
- **Sequences** — lazy, chainable generators (`pythoniota.iota`)
|
|
16
|
+
- **Regex** — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (`pythoniota.regex`)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Enums
|
|
21
|
+
|
|
22
|
+
### Go-style `iota`
|
|
23
|
+
|
|
24
|
+
`iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pythoniota import IotaEnum
|
|
28
|
+
|
|
29
|
+
class Color(IotaEnum):
|
|
30
|
+
Red = iota # 0
|
|
31
|
+
Green = iota # 1
|
|
32
|
+
Blue = iota # 2
|
|
33
|
+
|
|
34
|
+
Color.Green # 1
|
|
35
|
+
Color.names() # ['Red', 'Green', 'Blue']
|
|
36
|
+
Color.values() # [0, 1, 2]
|
|
37
|
+
list(Color) # [('Red', 0), ('Green', 1), ('Blue', 2)]
|
|
38
|
+
0 in Color # True (by value)
|
|
39
|
+
'Red' in Color # True (by name)
|
|
40
|
+
Color.from_value(2) # 'Blue'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`iota` works inside expressions — the value is the current counter:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
class Perm(IotaEnum):
|
|
47
|
+
Read = 1 << iota # 1
|
|
48
|
+
Write = 1 << iota # 2
|
|
49
|
+
Exec = 1 << iota # 4
|
|
50
|
+
All = Read | Write | Exec # 7
|
|
51
|
+
|
|
52
|
+
class Size(IotaEnum):
|
|
53
|
+
KB = 1 << (iota + 10) # 1024
|
|
54
|
+
MB = 1 << (iota + 10) # 2048
|
|
55
|
+
GB = 1 << (iota + 10) # 4096
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Skipping values & custom start/step
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
class Errno(IotaEnum):
|
|
62
|
+
EPERM = iota # 0
|
|
63
|
+
skip(2) # skip 1, 2
|
|
64
|
+
EBADF = iota # 3
|
|
65
|
+
|
|
66
|
+
class Port(IotaEnum):
|
|
67
|
+
_iota_start_ = 8000
|
|
68
|
+
_iota_step_ = 10
|
|
69
|
+
HTTP = iota # 8000
|
|
70
|
+
HTTPS = iota # 8010
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`_ = iota` also skips a single value (Go's blank identifier).
|
|
74
|
+
|
|
75
|
+
### Immutability, aliases, serialization
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
Color.Red = 99 # AttributeError: cannot modify enum member
|
|
79
|
+
Color.alias('R', 'Red') # Color.R == 0
|
|
80
|
+
Color.to_dict() # {'Red': 0, 'Green': 1, 'Blue': 2}
|
|
81
|
+
Color.to_json() # '{"Red": 0, "Green": 1, "Blue": 2}'
|
|
82
|
+
Color.from_json(s) # -> dict of members
|
|
83
|
+
Color.has('Red') # True
|
|
84
|
+
Color.get('Nope', -1) # -1
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `@unique` — reject duplicate values
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pythoniota import unique
|
|
91
|
+
|
|
92
|
+
@unique
|
|
93
|
+
class Status(IotaEnum):
|
|
94
|
+
Active = iota
|
|
95
|
+
Inactive = iota
|
|
96
|
+
# raises ValueError if two members share a value
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Ordered enums
|
|
100
|
+
|
|
101
|
+
Add `_ordered_ = True` to make members comparable by declaration order:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
class Priority(IotaEnum):
|
|
105
|
+
_ordered_ = True
|
|
106
|
+
Low = iota
|
|
107
|
+
Medium = iota
|
|
108
|
+
High = iota
|
|
109
|
+
|
|
110
|
+
Priority.Low < Priority.High # True
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Bit flags
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pythoniota import IotaBitFlags, FlagScope
|
|
117
|
+
|
|
118
|
+
class Access(IotaBitFlags):
|
|
119
|
+
Read = 1 << iota # 1
|
|
120
|
+
Write = 1 << iota # 2
|
|
121
|
+
Exec = 1 << iota # 4
|
|
122
|
+
|
|
123
|
+
flags = Access.Read | Access.Write
|
|
124
|
+
flags.has(Access.Read) # True
|
|
125
|
+
flags.has_all(Access.Read, Access.Exec) # False
|
|
126
|
+
flags.has_any(Access.Read, Access.Exec) # True
|
|
127
|
+
list(flags.decompose()) # [BitFlag(1), BitFlag(2)]
|
|
128
|
+
|
|
129
|
+
with FlagScope() as scope:
|
|
130
|
+
scope.grant(Access.Read, Access.Write)
|
|
131
|
+
scope.revoke(Access.Write)
|
|
132
|
+
scope.has(Access.Read) # True
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### String enums
|
|
136
|
+
|
|
137
|
+
Members resolve to their own name, or a custom format:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from pythoniota import IotaStringEnum
|
|
141
|
+
|
|
142
|
+
class Color(IotaStringEnum):
|
|
143
|
+
Red = iota # 'Red'
|
|
144
|
+
Blue = iota # 'Blue'
|
|
145
|
+
|
|
146
|
+
class Code(IotaStringEnum):
|
|
147
|
+
_format_ = "{name}_{index:03d}"
|
|
148
|
+
OK = iota # 'OK_000'
|
|
149
|
+
Error = iota # 'Error_001'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `@iota_enum` decorator
|
|
153
|
+
|
|
154
|
+
Turn a plain class into an enum:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pythoniota import iota_enum
|
|
158
|
+
|
|
159
|
+
@iota_enum
|
|
160
|
+
class Color:
|
|
161
|
+
Red = 0
|
|
162
|
+
Green = 1
|
|
163
|
+
Blue = 2
|
|
164
|
+
|
|
165
|
+
@iota_enum(ordered=True)
|
|
166
|
+
class Priority:
|
|
167
|
+
Low = 0
|
|
168
|
+
High = 1
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `match` / `case`
|
|
172
|
+
|
|
173
|
+
Enum members match by value:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
match color:
|
|
177
|
+
case Color.Red: ...
|
|
178
|
+
case Color.Blue: ...
|
|
179
|
+
case _: ...
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Sequences
|
|
185
|
+
|
|
186
|
+
`iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from pythoniota import iota
|
|
190
|
+
|
|
191
|
+
iota() # 0, 1, 2, ... (infinite)
|
|
192
|
+
iota(5) # 0, 1, 2, 3, 4
|
|
193
|
+
iota(2, 10, 2) # 2, 4, 6, 8
|
|
194
|
+
iota(5, map=lambda i: i*i) # 0, 1, 4, 9, 16
|
|
195
|
+
|
|
196
|
+
s = iota(10)
|
|
197
|
+
len(s) # 10 (O(1))
|
|
198
|
+
s[7] # 7 (O(1) arithmetic indexing)
|
|
199
|
+
s[2:5] # [2, 3, 4]
|
|
200
|
+
list(reversed(s)) # 9, 8, ... 0 (lazy)
|
|
201
|
+
5 in s # True (O(1))
|
|
202
|
+
s.take(3) # [0, 1, 2]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Operators
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
iota(3) + iota(3, 6) # concat: 0,1,2,3,4,5
|
|
209
|
+
iota(3) * 2 # repeat: 0,1,2,0,1,2
|
|
210
|
+
iota(3) | iota(3, 6) # interleave: 0,3,1,4,2,5
|
|
211
|
+
iota(3) @ iota(3, 6) # zip: (0,3),(1,4),(2,5)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Lazy combinators
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
iota(10).filter(lambda x: x % 2 == 0) # 0,2,4,6,8
|
|
218
|
+
iota().map(lambda x: x*x) # 0,1,4,9,...
|
|
219
|
+
iota().takewhile(lambda x: x < 5) # 0,1,2,3,4
|
|
220
|
+
iota(10).dropwhile(lambda x: x < 7) # 7,8,9
|
|
221
|
+
iota(4).pairwise() # (0,1),(1,2),(2,3)
|
|
222
|
+
iota(5).window(3) # (0,1,2),(1,2,3),(2,3,4)
|
|
223
|
+
iota(7).chunk(3) # [0,1,2],[3,4,5],[6]
|
|
224
|
+
iota(6).map(lambda x: x % 3).distinct() # 0,1,2
|
|
225
|
+
seq.flatten() # flatten one level
|
|
226
|
+
iota().enumerate() .accumulate() .zip(other)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Terminal operations
|
|
230
|
+
|
|
231
|
+
Reductions on plain arithmetic sequences are O(1); infinite sequences raise where they would not terminate.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
iota(1, 101).sum() # 5050 (closed-form, O(1))
|
|
235
|
+
iota(3, 10).min() # 3
|
|
236
|
+
iota(3, 10).max() # 9
|
|
237
|
+
iota(0, 10, 2).count() # 5
|
|
238
|
+
iota(5).last() # 4
|
|
239
|
+
iota(10).nth(3) # 3
|
|
240
|
+
iota().first() # 0
|
|
241
|
+
iota().find(lambda x: x > 100) # 101
|
|
242
|
+
iota(1, 5).all(lambda x: x > 0) # True
|
|
243
|
+
iota(5).any(lambda x: x == 3) # True
|
|
244
|
+
iota(3).reduce(lambda a, b: a + b) # 3
|
|
245
|
+
|
|
246
|
+
iota(3).to_list() # [0, 1, 2]
|
|
247
|
+
iota(3).to_tuple() # (0, 1, 2)
|
|
248
|
+
iota(3).to_set() # {0, 1, 2}
|
|
249
|
+
iota(3).to_dict(value=lambda x: x*x) # {0: 0, 1: 1, 2: 4}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Async iteration
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
async for x in iota(5):
|
|
256
|
+
...
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Recipes
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from pythoniota.recipes import (
|
|
265
|
+
fibonacci, lucas, factorial, catalan, harmonic,
|
|
266
|
+
triangle, powers, geometric, primes, primes_sieve,
|
|
267
|
+
pascal_row, collatz, repeat, cycle,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
fibonacci(10).to_list() # 0,1,1,2,3,5,8,13,21,34
|
|
271
|
+
lucas(7).to_list() # 2,1,3,4,7,11,18
|
|
272
|
+
factorial(6).to_list() # 1,1,2,6,24,120
|
|
273
|
+
catalan(6).to_list() # 1,1,2,5,14,42
|
|
274
|
+
harmonic(4).to_list() # 1.0, 1.5, 1.833..., 2.083...
|
|
275
|
+
triangle(5).to_list() # 0,1,3,6,10
|
|
276
|
+
powers(2, 8).to_list() # 1,2,4,...,128
|
|
277
|
+
geometric(3, 2, 4).to_list() # 3,6,12,24
|
|
278
|
+
primes(5).to_list() # 2,3,5,7,11 (first n primes)
|
|
279
|
+
primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19 (< limit, Eratosthenes)
|
|
280
|
+
pascal_row(4).to_list() # 1,4,6,4,1
|
|
281
|
+
collatz(6).to_list() # 6,3,10,5,16,8,4,2,1
|
|
282
|
+
repeat('x', 3).to_list() # ['x','x','x']
|
|
283
|
+
cycle([1,2], 5).to_list() # 1,2,1,2,1
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Safe expression evaluation
|
|
291
|
+
|
|
292
|
+
`safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from pythoniota import safe_eval
|
|
296
|
+
|
|
297
|
+
safe_eval("2 ** 10") # 1024
|
|
298
|
+
safe_eval("a * b + 1", {"a": 3, "b": 4}) # 13
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Regex
|
|
304
|
+
|
|
305
|
+
A from-scratch regular-expression engine. Unlike Python's backtracking `re`, it
|
|
306
|
+
compiles the pattern to a tiny bytecode and runs it with a **Pike VM** (a
|
|
307
|
+
thread-list NFA simulation), so matching is linear in `len(text) × len(program)`
|
|
308
|
+
— pathological patterns like `(a+)+$` finish instantly instead of hanging.
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from pythoniota import regex
|
|
312
|
+
|
|
313
|
+
m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
|
|
314
|
+
m.groups() # ('2026', '08', '02')
|
|
315
|
+
|
|
316
|
+
regex.findall(r"\w+", "hello world") # ['hello', 'world']
|
|
317
|
+
regex.sub(r"\s+", "_", "a b c") # 'a_b_c'
|
|
318
|
+
regex.split(r"[,;]\s*", "a, b; c,d") # ['a', 'b', 'c', 'd']
|
|
319
|
+
|
|
320
|
+
named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
|
|
321
|
+
named.groupdict() # {'user': 'user', 'host': 'host'}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Supported: literals, `.`, `* + ?` and their lazy forms `*? +? ??`, alternation
|
|
325
|
+
`|`, groups `(...)` / non-capturing `(?:...)` / named `(?P<name>...)`, lookahead
|
|
326
|
+
`(?=...)` / `(?!...)`, character classes `[...]`, counted repeats `{m,n}`,
|
|
327
|
+
shorthands `\d \w \s` (and `\D \W \S`), word boundaries `\b \B`, and anchors
|
|
328
|
+
`^ $ \A \Z`. Flags: `IGNORECASE`, `MULTILINE`, `DOTALL`, `VERBOSE` (also inline
|
|
329
|
+
`(?imsx)`). API mirrors `re`: `compile`, `match`, `fullmatch`, `search`,
|
|
330
|
+
`findall`, `finditer`, `sub`, `split`, `escape`, with `Match.group` / `groups` /
|
|
331
|
+
`span` / `groupdict`.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE) # ['AbCdEf']
|
|
335
|
+
regex.findall(r"\d+(?=px)", "10px 20em 30px") # ['10', '30'] (lookahead)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Backreferences (`\1`) are intentionally *not* supported: they can't be matched
|
|
339
|
+
by a finite automaton in linear time — that's the price of the no-backtracking
|
|
340
|
+
guarantee (the same trade-off Go's `regexp` and RE2 make).
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
# no ReDoS: this returns in microseconds, where re takes ~13s at length 28
|
|
344
|
+
regex.search(r"(a+)+$", "a" * 40 + "!") # None, instantly
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|