PythonIota 1.7.1__tar.gz → 1.9.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.9.0/MANIFEST.in +2 -0
- pythoniota-1.9.0/PKG-INFO +659 -0
- pythoniota-1.9.0/README.md +642 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/pyproject.toml +4 -1
- pythoniota-1.9.0/src/PythonIota.egg-info/PKG-INFO +659 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/SOURCES.txt +13 -1
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/__init__.py +3 -2
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/__init__.py +13 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_argument.py +34 -9
- pythoniota-1.9.0/src/pythoniota/cli/_completion.py +222 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_formatter.py +12 -3
- pythoniota-1.9.0/src/pythoniota/cli/_parser.py +469 -0
- pythoniota-1.9.0/src/pythoniota/cli/_sources.py +147 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/enum.py +11 -1
- pythoniota-1.9.0/src/pythoniota/regex/__init__.py +74 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/regex/_compiler.py +27 -8
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/regex/_parser.py +4 -0
- pythoniota-1.9.0/src/pythoniota/regex/_pattern.py +280 -0
- pythoniota-1.9.0/src/pythoniota/regex/_vm.py +214 -0
- pythoniota-1.9.0/src/pythoniota/sequence.py +803 -0
- pythoniota-1.9.0/tests/__init__.py +0 -0
- pythoniota-1.9.0/tests/test_cli.py +558 -0
- pythoniota-1.9.0/tests/test_cli_completion.py +271 -0
- pythoniota-1.9.0/tests/test_cli_groups_version.py +347 -0
- pythoniota-1.9.0/tests/test_cli_scripts.py +316 -0
- pythoniota-1.9.0/tests/test_cli_sources.py +431 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_enum_enhanced.py +41 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_integration.py +5 -1
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_recipes.py +65 -1
- pythoniota-1.9.0/tests/test_regex_budget.py +163 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_regex_extended.py +39 -0
- pythoniota-1.9.0/tests/test_regex_search.py +169 -0
- pythoniota-1.9.0/tests/test_sequence.py +576 -0
- pythoniota-1.9.0/tests/test_sequence_typing.py +217 -0
- pythoniota-1.9.0/tests/test_sequence_utilities.py +507 -0
- pythoniota-1.9.0/tests/test_workflows.py +62 -0
- pythoniota-1.7.1/MANIFEST.in +0 -1
- pythoniota-1.7.1/PKG-INFO +0 -424
- pythoniota-1.7.1/README.md +0 -407
- pythoniota-1.7.1/src/PythonIota.egg-info/PKG-INFO +0 -424
- pythoniota-1.7.1/src/pythoniota/cli/_parser.py +0 -321
- pythoniota-1.7.1/src/pythoniota/regex/__init__.py +0 -66
- pythoniota-1.7.1/src/pythoniota/regex/_pattern.py +0 -210
- pythoniota-1.7.1/src/pythoniota/regex/_vm.py +0 -129
- pythoniota-1.7.1/src/pythoniota/sequence.py +0 -461
- pythoniota-1.7.1/tests/test_cli.py +0 -244
- pythoniota-1.7.1/tests/test_sequence.py +0 -153
- {pythoniota-1.7.1 → pythoniota-1.9.0}/setup.cfg +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/dependency_links.txt +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/PythonIota.egg-info/top_level.txt +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_bitflag.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_compat.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/_safe_eval.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_errors.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/cli/_namespace.py +0 -0
- /pythoniota-1.7.1/tests/__init__.py → /pythoniota-1.9.0/src/pythoniota/py.typed +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/src/pythoniota/recipes.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_advanced.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_bitflags.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_compat.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill2.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_coverage_fill3.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_enum.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_new_features.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_regex.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_safe_eval.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_serialization.py +0 -0
- {pythoniota-1.7.1 → pythoniota-1.9.0}/tests/test_string_enum.py +0 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: PythonIota
|
|
3
|
+
Version: 1.9.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 Pike-VM regex engine.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install PythonIota
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Python 3.10+.
|
|
28
|
+
|
|
29
|
+
## 1.9.0
|
|
30
|
+
|
|
31
|
+
### New features
|
|
32
|
+
|
|
33
|
+
- Regex searches skip impossible starts using a conservative literal prefix;
|
|
34
|
+
diagnostics expose the prefix and account for prefilter operations.
|
|
35
|
+
- CLI mutually exclusive groups, version output and static completion scripts for
|
|
36
|
+
Bash, Zsh and PowerShell extend the existing metadata-only completion API.
|
|
37
|
+
- Sequence pipelines add bounded `limit`, grouping, Top-K, partitioning, iterator
|
|
38
|
+
copies and combinatorics, with protection for known non-exhausting inputs.
|
|
39
|
+
|
|
40
|
+
### Fixes
|
|
41
|
+
|
|
42
|
+
- Pattern backreferences now fail explicitly instead of silently matching digits.
|
|
43
|
+
- Out-of-range regex positions are clamped to the text, matching `re` semantics.
|
|
44
|
+
- Counted nullable repetitions stop after zero-width iterations without losing
|
|
45
|
+
the preferred non-empty alternative during iteration.
|
|
46
|
+
|
|
47
|
+
## 1.8.0
|
|
48
|
+
|
|
49
|
+
### New features
|
|
50
|
+
|
|
51
|
+
- Generic, single-pass `IotaIterator` pipelines support continuous chaining.
|
|
52
|
+
- CLI options support explicit JSON/mapping configuration, opt-in environment
|
|
53
|
+
values and metadata-only completion candidates.
|
|
54
|
+
- Regex operations support shared step/time budgets and structured diagnostics.
|
|
55
|
+
|
|
56
|
+
### Fixes
|
|
57
|
+
|
|
58
|
+
- Mapping derived sequences preserves their lazy iteration, element types and
|
|
59
|
+
finite/infinite behavior; `filter` still returns an iterator.
|
|
60
|
+
- Empty regex matches, conditional capture states and zero-width repetitions
|
|
61
|
+
retain the appropriate alternatives and iteration order.
|
|
62
|
+
- Parent CLI positionals are validated before subcommand dispatch.
|
|
63
|
+
- Enum aliases cannot overwrite existing or reserved names; alias identifiers
|
|
64
|
+
must already be NFKC-normalized to agree with Python attribute syntax.
|
|
65
|
+
- Sequence APIs carry generic element types, and distributions include `py.typed`.
|
|
66
|
+
|
|
67
|
+
## 1.7.1 fixes
|
|
68
|
+
|
|
69
|
+
- Full matching now considers complete alternatives instead of rejecting a
|
|
70
|
+
valid match after selecting a shorter branch.
|
|
71
|
+
- Ordered enum members now hash consistently with equal numeric values and
|
|
72
|
+
equal members of other enums, preserving dictionary and set lookups.
|
|
73
|
+
|
|
74
|
+
**What's inside**
|
|
75
|
+
|
|
76
|
+
- **Enums** — Go-style `iota`, bit flags, string enums (`pythoniota.IotaEnum`, ...)
|
|
77
|
+
- **Sequences** — lazy, chainable generators (`pythoniota.iota`)
|
|
78
|
+
- **Regex** — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (`pythoniota.regex`)
|
|
79
|
+
- **CLI** — a from-scratch `argparse`-style argument parser (`pythoniota.cli`)
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Enums
|
|
84
|
+
|
|
85
|
+
### Go-style `iota`
|
|
86
|
+
|
|
87
|
+
`iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pythoniota import IotaEnum
|
|
91
|
+
|
|
92
|
+
class Color(IotaEnum):
|
|
93
|
+
Red = iota # 0
|
|
94
|
+
Green = iota # 1
|
|
95
|
+
Blue = iota # 2
|
|
96
|
+
|
|
97
|
+
Color.Green # 1
|
|
98
|
+
Color.names() # ['Red', 'Green', 'Blue']
|
|
99
|
+
Color.values() # [0, 1, 2]
|
|
100
|
+
list(Color) # [('Red', 0), ('Green', 1), ('Blue', 2)]
|
|
101
|
+
0 in Color # True (by value)
|
|
102
|
+
'Red' in Color # True (by name)
|
|
103
|
+
Color.from_value(2) # 'Blue'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`iota` works inside expressions — the value is the current counter:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
class Perm(IotaEnum):
|
|
110
|
+
Read = 1 << iota # 1
|
|
111
|
+
Write = 1 << iota # 2
|
|
112
|
+
Exec = 1 << iota # 4
|
|
113
|
+
All = Read | Write | Exec # 7
|
|
114
|
+
|
|
115
|
+
class Size(IotaEnum):
|
|
116
|
+
KB = 1 << (iota + 10) # 1024
|
|
117
|
+
MB = 1 << (iota + 10) # 2048
|
|
118
|
+
GB = 1 << (iota + 10) # 4096
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Skipping values & custom start/step
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
class Errno(IotaEnum):
|
|
125
|
+
EPERM = iota # 0
|
|
126
|
+
skip(2) # skip 1, 2
|
|
127
|
+
EBADF = iota # 3
|
|
128
|
+
|
|
129
|
+
class Port(IotaEnum):
|
|
130
|
+
_iota_start_ = 8000
|
|
131
|
+
_iota_step_ = 10
|
|
132
|
+
HTTP = iota # 8000
|
|
133
|
+
HTTPS = iota # 8010
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`_ = iota` also skips a single value (Go's blank identifier).
|
|
137
|
+
|
|
138
|
+
### Immutability, aliases, serialization
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
Color.Red = 99 # AttributeError: cannot modify enum member
|
|
142
|
+
Color.alias('R', 'Red') # Color.R == 0
|
|
143
|
+
Color.to_dict() # {'Red': 0, 'Green': 1, 'Blue': 2}
|
|
144
|
+
Color.to_json() # '{"Red": 0, "Green": 1, "Blue": 2}'
|
|
145
|
+
Color.from_json(s) # -> dict of members
|
|
146
|
+
Color.has('Red') # True
|
|
147
|
+
Color.get('Nope', -1) # -1
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Alias names must be public Python identifiers. Existing members, aliases,
|
|
151
|
+
methods and reserved/internal names cannot be overwritten; rejected aliases
|
|
152
|
+
leave the enum unchanged.
|
|
153
|
+
|
|
154
|
+
### `@unique` — reject duplicate values
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pythoniota import unique
|
|
158
|
+
|
|
159
|
+
@unique
|
|
160
|
+
class Status(IotaEnum):
|
|
161
|
+
Active = iota
|
|
162
|
+
Inactive = iota
|
|
163
|
+
# raises ValueError if two members share a value
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Ordered enums
|
|
167
|
+
|
|
168
|
+
Add `_ordered_ = True` to make members comparable by declaration order:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
class Priority(IotaEnum):
|
|
172
|
+
_ordered_ = True
|
|
173
|
+
Low = iota
|
|
174
|
+
Medium = iota
|
|
175
|
+
High = iota
|
|
176
|
+
|
|
177
|
+
Priority.Low < Priority.High # True
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Bit flags
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from pythoniota import IotaBitFlags, FlagScope
|
|
184
|
+
|
|
185
|
+
class Access(IotaBitFlags):
|
|
186
|
+
Read = 1 << iota # 1
|
|
187
|
+
Write = 1 << iota # 2
|
|
188
|
+
Exec = 1 << iota # 4
|
|
189
|
+
|
|
190
|
+
flags = Access.Read | Access.Write
|
|
191
|
+
flags.has(Access.Read) # True
|
|
192
|
+
flags.has_all(Access.Read, Access.Exec) # False
|
|
193
|
+
flags.has_any(Access.Read, Access.Exec) # True
|
|
194
|
+
list(flags.decompose()) # [BitFlag(1), BitFlag(2)]
|
|
195
|
+
|
|
196
|
+
with FlagScope() as scope:
|
|
197
|
+
scope.grant(Access.Read, Access.Write)
|
|
198
|
+
scope.revoke(Access.Write)
|
|
199
|
+
scope.has(Access.Read) # True
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### String enums
|
|
203
|
+
|
|
204
|
+
Members resolve to their own name, or a custom format:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from pythoniota import IotaStringEnum
|
|
208
|
+
|
|
209
|
+
class Color(IotaStringEnum):
|
|
210
|
+
Red = iota # 'Red'
|
|
211
|
+
Blue = iota # 'Blue'
|
|
212
|
+
|
|
213
|
+
class Code(IotaStringEnum):
|
|
214
|
+
_format_ = "{name}_{index:03d}"
|
|
215
|
+
OK = iota # 'OK_000'
|
|
216
|
+
Error = iota # 'Error_001'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `@iota_enum` decorator
|
|
220
|
+
|
|
221
|
+
Turn a plain class into an enum:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from pythoniota import iota_enum
|
|
225
|
+
|
|
226
|
+
@iota_enum
|
|
227
|
+
class Color:
|
|
228
|
+
Red = 0
|
|
229
|
+
Green = 1
|
|
230
|
+
Blue = 2
|
|
231
|
+
|
|
232
|
+
@iota_enum(ordered=True)
|
|
233
|
+
class Priority:
|
|
234
|
+
Low = 0
|
|
235
|
+
High = 1
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `match` / `case`
|
|
239
|
+
|
|
240
|
+
Enum members match by value:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
match color:
|
|
244
|
+
case Color.Red: ...
|
|
245
|
+
case Color.Blue: ...
|
|
246
|
+
case _: ...
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Sequences
|
|
252
|
+
|
|
253
|
+
`iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pythoniota import iota
|
|
257
|
+
|
|
258
|
+
iota() # 0, 1, 2, ... (infinite)
|
|
259
|
+
iota(5) # 0, 1, 2, 3, 4
|
|
260
|
+
iota(2, 10, 2) # 2, 4, 6, 8
|
|
261
|
+
iota(5, map=lambda i: i*i) # 0, 1, 4, 9, 16
|
|
262
|
+
|
|
263
|
+
s = iota(10)
|
|
264
|
+
len(s) # 10 (O(1))
|
|
265
|
+
s[7] # 7 (O(1) arithmetic indexing)
|
|
266
|
+
s[2:5] # [2, 3, 4]
|
|
267
|
+
list(reversed(s)) # 9, 8, ... 0 (lazy)
|
|
268
|
+
5 in s # True (O(1))
|
|
269
|
+
s.take(3) # [0, 1, 2]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Operators
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
iota(3) + iota(3, 6) # concat: 0,1,2,3,4,5
|
|
276
|
+
iota(3) * 2 # repeat: 0,1,2,0,1,2
|
|
277
|
+
iota(3) | iota(3, 6) # interleave: 0,3,1,4,2,5
|
|
278
|
+
iota(3) @ iota(3, 6) # zip: (0,3),(1,4),(2,5)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Lazy combinators
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
iota(10).filter(lambda x: x % 2 == 0) # 0,2,4,6,8
|
|
285
|
+
iota().map(lambda x: x*x) # 0,1,4,9,...
|
|
286
|
+
iota().takewhile(lambda x: x < 5) # 0,1,2,3,4
|
|
287
|
+
iota(10).dropwhile(lambda x: x < 7) # 7,8,9
|
|
288
|
+
iota(4).pairwise() # (0,1),(1,2),(2,3)
|
|
289
|
+
iota(5).window(3) # (0,1,2),(1,2,3),(2,3,4)
|
|
290
|
+
iota(7).chunk(3) # [0,1,2],[3,4,5],[6]
|
|
291
|
+
iota(6).map(lambda x: x % 3).distinct() # 0,1,2
|
|
292
|
+
seq.flatten() # flatten one level
|
|
293
|
+
iota().enumerate() .accumulate() .zip(other)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Chainable iterator pipelines
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from pythoniota import IotaIterator, iota
|
|
300
|
+
|
|
301
|
+
windows = iota(20).filter(lambda x: x % 2 == 0).map(str).window(3)
|
|
302
|
+
windows.take(2) # [('0', '2', '4'), ('2', '4', '6')]
|
|
303
|
+
next(windows) # ('4', '6', '8'): continues the same stream
|
|
304
|
+
IotaIterator([1, 2, 3]).map(lambda x: x * 10).to_list() # [10, 20, 30]
|
|
305
|
+
iota().filter(lambda x: x % 2 == 0).map(str).take(3) # ['0', '2', '4']
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`filter`, `accumulate`, `enumerate`, `zip`, `chunk`, `window`, `takewhile`,
|
|
309
|
+
`dropwhile`, `pairwise`, `flatten` and `distinct` return `IotaIterator` pipelines,
|
|
310
|
+
which can continue chaining. They are lazy and single-pass (`iter(pipe) is pipe`),
|
|
311
|
+
with element types preserved through generic transformations. `take(n)` still
|
|
312
|
+
returns a list. Chunk/window sizes must be positive and are checked before consumption.
|
|
313
|
+
|
|
314
|
+
Base sequences and their `.map()` results remain reusable. Iterator pipelines
|
|
315
|
+
have no length, but expose `infinite`: `True` for known non-exhausting sources,
|
|
316
|
+
`False` for known finite sources, and `None` when unknown. Exhaustive terminals
|
|
317
|
+
reject known non-exhausting pipelines before consuming them. Filtering/distinct
|
|
318
|
+
can yield few values without ever exhausting their infinite input; these retain
|
|
319
|
+
the protection. Arbitrary generators and early-stopping `takewhile` may remain
|
|
320
|
+
unknown, so this is not a general termination detector.
|
|
321
|
+
|
|
322
|
+
Use `.limit(n)` to keep a bounded prefix chainable (`take(n)` still returns a list):
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
iota().filter(lambda x: x % 2 == 0).limit(5).sum() # 20
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`limit` bounds the number of returned elements, not the time to produce one:
|
|
329
|
+
`iota().filter(lambda x: False).limit(1)` still cannot yield a value. Short-circuit
|
|
330
|
+
operations (`first`, `find`, `any`, `all`, `take`) remain available on infinite input.
|
|
331
|
+
|
|
332
|
+
### Grouping, selection and iterator utilities
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
iota(6).group_by(lambda x: x % 2) # {0: [0, 2, 4], 1: [1, 3, 5]}
|
|
336
|
+
iota(10).top_k(3) # [9, 8, 7]
|
|
337
|
+
iota(10).top_k(3, largest=False) # [0, 1, 2]
|
|
338
|
+
iota(2).zip_longest(["a"], fillvalue="-").to_list() # [(0, 'a'), (1, '-')]
|
|
339
|
+
iota(2).product(["a", "b"]).to_list() # [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
|
|
340
|
+
iota(3).permutations(2).take(3) # [(0, 1), (0, 2), (1, 0)]
|
|
341
|
+
|
|
342
|
+
odds, evens = iota(6).partition(lambda x: x % 2 == 0)
|
|
343
|
+
odds.to_list(), evens.to_list() # ([1, 3, 5], [0, 2, 4])
|
|
344
|
+
left, right = iota(3).tee()
|
|
345
|
+
left.take(2), right.to_list() # ([0, 1], [0, 1, 2])
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`group_by(key=None)` groups globally into lists, preserving key/element insertion
|
|
349
|
+
order; unlike `itertools.groupby`, equal keys need not be adjacent. `top_k` supports
|
|
350
|
+
`key=`, preserves input order for ties and keeps O(k) selection storage, but must
|
|
351
|
+
consume the entire finite input. `top_k(0)` returns immediately without consuming.
|
|
352
|
+
Neither operation can compute a global result from an infinite stream.
|
|
353
|
+
|
|
354
|
+
`partition` returns false/true branches and evaluates its predicate once per item.
|
|
355
|
+
Like `tee`, it buffers when consumers advance at different rates; that buffer can
|
|
356
|
+
grow without bound. `product` and `permutations` defer input consumption until
|
|
357
|
+
iteration, then cache their inputs like `itertools`; they reject known infinite
|
|
358
|
+
inputs and are not constant-memory streaming operations.
|
|
359
|
+
|
|
360
|
+
### Terminal operations
|
|
361
|
+
|
|
362
|
+
Reductions on plain arithmetic sequences are O(1); known-infinite base sequences
|
|
363
|
+
reject exhaustive terminals. Iterator pipelines consume their remaining elements.
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
iota(1, 101).sum() # 5050 (closed-form, O(1))
|
|
367
|
+
iota(3, 10).min() # 3
|
|
368
|
+
iota(3, 10).max() # 9
|
|
369
|
+
iota(0, 10, 2).count() # 5
|
|
370
|
+
iota(5).last() # 4
|
|
371
|
+
iota(10).nth(3) # 3
|
|
372
|
+
iota().first() # 0
|
|
373
|
+
iota().find(lambda x: x > 100) # 101
|
|
374
|
+
iota(1, 5).all(lambda x: x > 0) # True
|
|
375
|
+
iota(5).any(lambda x: x == 3) # True
|
|
376
|
+
iota(3).reduce(lambda a, b: a + b) # 3
|
|
377
|
+
|
|
378
|
+
iota(3).to_list() # [0, 1, 2]
|
|
379
|
+
iota(3).to_tuple() # (0, 1, 2)
|
|
380
|
+
iota(3).to_set() # {0, 1, 2}
|
|
381
|
+
iota(3).to_dict(value=lambda x: x*x) # {0: 0, 1: 1, 2: 4}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Async iteration
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
async for x in iota(5):
|
|
388
|
+
...
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Recipes
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from pythoniota.recipes import (
|
|
397
|
+
fibonacci, lucas, factorial, catalan, harmonic,
|
|
398
|
+
triangle, powers, geometric, primes, primes_sieve,
|
|
399
|
+
pascal_row, collatz, repeat, cycle,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
fibonacci(10).to_list() # 0,1,1,2,3,5,8,13,21,34
|
|
403
|
+
lucas(7).to_list() # 2,1,3,4,7,11,18
|
|
404
|
+
factorial(6).to_list() # 1,1,2,6,24,120
|
|
405
|
+
catalan(6).to_list() # 1,1,2,5,14,42
|
|
406
|
+
harmonic(4).to_list() # 1.0, 1.5, 1.833..., 2.083...
|
|
407
|
+
triangle(5).to_list() # 0,1,3,6,10
|
|
408
|
+
powers(2, 8).to_list() # 1,2,4,...,128
|
|
409
|
+
geometric(3, 2, 4).to_list() # 3,6,12,24
|
|
410
|
+
primes(5).to_list() # 2,3,5,7,11 (first n primes)
|
|
411
|
+
primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19 (< limit, Eratosthenes)
|
|
412
|
+
pascal_row(4).to_list() # 1,4,6,4,1
|
|
413
|
+
collatz(6).to_list() # 6,3,10,5,16,8,4,2,1
|
|
414
|
+
repeat('x', 3).to_list() # ['x','x','x']
|
|
415
|
+
cycle([1,2], 5).to_list() # 1,2,1,2,1
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Safe expression evaluation
|
|
423
|
+
|
|
424
|
+
`safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from pythoniota import safe_eval
|
|
428
|
+
|
|
429
|
+
safe_eval("2 ** 10") # 1024
|
|
430
|
+
safe_eval("a * b + 1", {"a": 3, "b": 4}) # 13
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Regex
|
|
436
|
+
|
|
437
|
+
A from-scratch regular-expression engine that runs bytecode with an ordered
|
|
438
|
+
**Pike VM**. Plain patterns such as `(a+)+$` avoid catastrophic backtracking and
|
|
439
|
+
run in linear time in the text length for a fixed pattern. This is not a
|
|
440
|
+
universal complexity guarantee: lookaround can rescan suffixes, and conditionals
|
|
441
|
+
can retain multiple capture-participation states. Use explicit `max_steps` and
|
|
442
|
+
`timeout` limits when matching potentially expensive patterns.
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
from pythoniota import regex
|
|
446
|
+
|
|
447
|
+
m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
|
|
448
|
+
m.groups() # ('2026', '08', '02')
|
|
449
|
+
|
|
450
|
+
regex.findall(r"\w+", "hello world") # ['hello', 'world']
|
|
451
|
+
regex.sub(r"\s+", "_", "a b c") # 'a_b_c'
|
|
452
|
+
regex.split(r"[,;]\s*", "a, b; c,d") # ['a', 'b', 'c', 'd']
|
|
453
|
+
|
|
454
|
+
named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
|
|
455
|
+
named.groupdict() # {'user': 'user', 'host': 'host'}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Supported: literals, `.`, `* + ?` and their lazy forms `*? +? ??`, alternation
|
|
459
|
+
`|`, groups `(...)` / non-capturing `(?:...)` / named `(?P<name>...)`, lookahead
|
|
460
|
+
`(?=...)` / `(?!...)`, fixed-width lookbehind `(?<=...)` / `(?<!...)`, conditionals
|
|
461
|
+
`(?(id/name)yes|no)`, character classes `[...]`, counted repeats `{m,n}`,
|
|
462
|
+
shorthands `\d \w \s` (and `\D \W \S`), word boundaries `\b \B`, anchors
|
|
463
|
+
`^ $ \A \Z`, and hex escapes `\xHH` / `\uHHHH`. Flags: `IGNORECASE`, `MULTILINE`,
|
|
464
|
+
`DOTALL`, `VERBOSE` (also inline `(?imsx)`). API mirrors `re`: `compile`, `match`,
|
|
465
|
+
`fullmatch`, `search`, `findall`, `finditer`, `sub`, `subn`, `split`, `escape`,
|
|
466
|
+
with `Match.group` / `groups` / `span` / `groupdict` / `expand`.
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE) # ['AbCdEf']
|
|
470
|
+
regex.findall(r"\d+(?=px)", "10px 20em 30px") # ['10', '30'] (lookahead)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Pattern backreferences (`\1`, `(?P=name)`) raise `RegexError`; this is an NFA-based
|
|
474
|
+
engine rather than a general backtracking engine. Replacement templates still
|
|
475
|
+
support backreferences. Patterns are text strings, not bytes. Empty matches follow
|
|
476
|
+
the iteration and splitting behavior of Python 3.10–3.13 `re`, including a non-empty
|
|
477
|
+
match immediately after an empty one at the same position.
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
regex.search(r"(a+)+$", "a" * 40 + "!") # None, without backtracking
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Execution limits and diagnostics
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
pattern = regex.compile(r"(?=a*$)")
|
|
487
|
+
try:
|
|
488
|
+
result = pattern.search("a" * 100 + "!", max_steps=1000, timeout=0.1)
|
|
489
|
+
except regex.RegexLimitError as error:
|
|
490
|
+
print(error.reason, error.steps)
|
|
491
|
+
|
|
492
|
+
structure = pattern.explain() # JSON-serializable bytecode, groups and nested assertions
|
|
493
|
+
report = pattern.diagnose("aaaa!", max_steps=1000)
|
|
494
|
+
print(report["status"], report["steps"], report["op_counts"])
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Search and iteration use a conservative mandatory literal prefix when available.
|
|
498
|
+
`explain()["literal_prefix"]` shows it; case-insensitive patterns and leading
|
|
499
|
+
branching/nullable patterns fall back to the normal VM. Existing higher-priority
|
|
500
|
+
threads keep running: the optimization does not restart the VM for each candidate.
|
|
501
|
+
|
|
502
|
+
All matching, iteration, splitting and replacement functions accept keyword-only
|
|
503
|
+
`max_steps` (nonnegative integer) and `timeout` (nonnegative seconds), defaulting
|
|
504
|
+
to no limits. A step is a bytecode analysis/visit or one native prefix lookup
|
|
505
|
+
(`PREFILTER`), including epsilon closure and consuming states; counts are
|
|
506
|
+
implementation-specific, not a portable time unit or a per-character work bound.
|
|
507
|
+
Nested lookarounds and all matches within one call share a single budget.
|
|
508
|
+
`finditer` starts its timeout when created, including pauses between yields.
|
|
509
|
+
|
|
510
|
+
Limit exhaustion raises `RegexLimitError`, never a false no-match result.
|
|
511
|
+
`diagnose` supports `match`, `fullmatch` and `search`, instead returning an explicit
|
|
512
|
+
`limit_exceeded` status with its reason, counters, peak pending threads and elapsed
|
|
513
|
+
time. `explain` and `diagnose` also exist as module-level functions.
|
|
514
|
+
|
|
515
|
+
Limits apply after pattern compilation. Timeouts are cooperative: they cannot
|
|
516
|
+
preempt Python replacement callbacks, compilation or a single long native
|
|
517
|
+
operation; elapsed time is checked again after callbacks. For hard isolation of
|
|
518
|
+
untrusted patterns/callbacks, use a separate process and external resource limits.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## CLI
|
|
523
|
+
|
|
524
|
+
A faithful subset of the standard library's `argparse`, built from scratch:
|
|
525
|
+
|
|
526
|
+
```python
|
|
527
|
+
from pythoniota import cli
|
|
528
|
+
|
|
529
|
+
p = cli.ArgumentParser(prog="greet", description="say hello")
|
|
530
|
+
p.add_argument("name")
|
|
531
|
+
p.add_argument("-n", "--times", type=int, default=1)
|
|
532
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
533
|
+
|
|
534
|
+
args = p.parse_args(["world", "--times", "3", "-v"])
|
|
535
|
+
args.name, args.times, args.verbose # ('world', 3, True)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Supported:
|
|
539
|
+
|
|
540
|
+
- positional & optional arguments, `nargs` (`N` / `?` / `*` / `+`)
|
|
541
|
+
- `type` / `choices` / `default` / `required`
|
|
542
|
+
- actions: `store`, `store_true`, `store_false`, `store_const`, `append`, `count`
|
|
543
|
+
- `--opt=value`, clustered short flags (`-abc`), the `--` end-of-options marker
|
|
544
|
+
- an auto-generated `-h/--help`, and sub-commands via `add_subparsers`
|
|
545
|
+
|
|
546
|
+
Bad input prints a usage line and raises `SystemExit(2)` — the same contract as
|
|
547
|
+
argparse, so it drops into scripts and tests unchanged. `parse_known_args`
|
|
548
|
+
returns `(namespace, extras)` instead of erroring on unknown tokens.
|
|
549
|
+
|
|
550
|
+
### Mutually exclusive options and version output
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
p = cli.ArgumentParser(prog="convert")
|
|
554
|
+
mode = p.add_mutually_exclusive_group(required=True)
|
|
555
|
+
mode.add_argument("--json", action="store_true")
|
|
556
|
+
mode.add_argument("--text", action="store_true")
|
|
557
|
+
p.add_argument("--version", action="version", version="%(prog)s 1.0")
|
|
558
|
+
p.parse_args(["--json"])
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Groups accept optional arguments only. A required group needs exactly one explicit
|
|
562
|
+
member; an optional group permits zero or one. CLI, environment and configuration
|
|
563
|
+
values count as explicit, even `False` or zero; defaults and caller `Namespace`
|
|
564
|
+
values do not. CLI overrides lower sources for the same destination, not another
|
|
565
|
+
group member: CLI `--json` plus config `text=true` is a conflict. Repeating the same
|
|
566
|
+
member is allowed. Group members cannot themselves be required or share a destination.
|
|
567
|
+
|
|
568
|
+
A valid version request prints to stdout and exits with status 0 without reading
|
|
569
|
+
configuration or converting unrelated values. It bypasses missing required options
|
|
570
|
+
but still respects argument consumption, subcommand boundaries and `--`.
|
|
571
|
+
|
|
572
|
+
### Sub-commands
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
p = cli.ArgumentParser(prog="git")
|
|
576
|
+
sub = p.add_subparsers(dest="cmd")
|
|
577
|
+
|
|
578
|
+
add = sub.add_parser("add", help="stage files")
|
|
579
|
+
add.add_argument("path")
|
|
580
|
+
add.add_argument("-f", "--force", action="store_true")
|
|
581
|
+
|
|
582
|
+
rm = sub.add_parser("rm", help="remove files")
|
|
583
|
+
rm.add_argument("path")
|
|
584
|
+
|
|
585
|
+
args = p.parse_args(["add", "README.md", "-f"])
|
|
586
|
+
args.cmd, args.path, args.force # ('add', 'README.md', True)
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Parent positionals declared before `add_subparsers` are consumed and validated
|
|
590
|
+
before the command. Parent options may occur before the command; trailing
|
|
591
|
+
options are parsed only by the selected child. As with argparse, variable
|
|
592
|
+
`nargs` are greedy and command names are not special delimiters, so prefer
|
|
593
|
+
fixed-count parent positionals for predictable subcommand syntax.
|
|
594
|
+
|
|
595
|
+
### Configuration and environment values
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
p = cli.ArgumentParser(config={"port": 8000, "mode": "fast"}, env_prefix="APP_")
|
|
599
|
+
p.add_argument("--port", type=int, default=3000)
|
|
600
|
+
p.add_argument("--mode", choices=["fast", "slow"])
|
|
601
|
+
p.add_argument("--token", env_var="SERVICE_TOKEN")
|
|
602
|
+
args = p.parse_args(["--port", "9000"]) # --port overrides APP_PORT and config
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
`config` accepts a mapping or a UTF-8 JSON-object path, read again on each parse.
|
|
606
|
+
Keys are this parser's optional-argument destinations, not option spellings;
|
|
607
|
+
unknown keys and malformed files are errors. Sources do not fill positionals.
|
|
608
|
+
Precedence is explicit CLI > enabled environment > config > argument default;
|
|
609
|
+
only the winning value is converted and checked against `choices`. Required
|
|
610
|
+
options may be supplied by config/environment. Explicit caller `Namespace`
|
|
611
|
+
values are retained unless the CLI changes them, but do not by themselves
|
|
612
|
+
satisfy required options.
|
|
613
|
+
|
|
614
|
+
Environment lookup is opt-in via `env_prefix` or an exact `env_var`; children
|
|
615
|
+
configure their own sources with `add_parser(config=..., env_prefix=...)`.
|
|
616
|
+
Scalar environment values are literal strings. Multi-value options use JSON
|
|
617
|
+
arrays; `append` uses an array of occurrences (nested arrays for multi-value
|
|
618
|
+
occurrences). Boolean values accept true/false, yes/no, on/off and 1/0; counts
|
|
619
|
+
must be nonnegative integers. Explicit CLI append/count replaces lower sources,
|
|
620
|
+
while legacy accumulation from argument defaults is retained when no external
|
|
621
|
+
source applies. No configuration contents are executed as code or shell text.
|
|
622
|
+
|
|
623
|
+
### Completion candidates
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
p.complete([], "--po") # ['--port']
|
|
627
|
+
p.complete(["--mode"], "s") # ['slow']
|
|
628
|
+
p.complete([], "--mode=f") # ['--mode=fast']
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
`complete(tokens, prefix="")` accepts finished words without the program/current
|
|
632
|
+
word, then returns sorted, unique full replacement words for options, choices
|
|
633
|
+
and subcommands. It does not parse, read config/environment, call type converters,
|
|
634
|
+
execute shells or mutate parse state. Variable-length positionals are best-effort;
|
|
635
|
+
only built-in collections of plain scalar choices are enumerated, not generators
|
|
636
|
+
or custom objects. This is an adapter API for shells/editors: it does not install
|
|
637
|
+
shell hooks, quote candidates or perform filesystem completion.
|
|
638
|
+
|
|
639
|
+
### Static shell completion scripts
|
|
640
|
+
|
|
641
|
+
```python
|
|
642
|
+
script = p.completion_script("bash", command="convert")
|
|
643
|
+
# Also accepts "zsh" and "powershell"; save/load the returned text yourself.
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
This generates a quoted static snapshot of options, subcommands and finite scalar
|
|
647
|
+
choices. It does not run the application, read sources, call converters or change
|
|
648
|
+
shell configuration. Unlike `complete(tokens, prefix)`, the snapshot is not a
|
|
649
|
+
context-sensitive parser: it may suggest candidates from other options or commands.
|
|
650
|
+
Regenerate it after changing parser metadata. Shell metacharacters are emitted as
|
|
651
|
+
literal data, not executable fragments. Command names containing control characters
|
|
652
|
+
are rejected; such candidates are omitted. Zsh requires an initialized completion
|
|
653
|
+
system (`compinit`).
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## License
|
|
658
|
+
|
|
659
|
+
MIT
|