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