Metadata-Version: 2.2
Name: PythonIota
Version: 1.9.0
Summary: A zero-dependency Python toolkit: Go-style iota enums, sequence generators, a from-scratch regex engine, and an argparse-style CLI parser
Author: Equinox
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# PythonIota

A **zero-dependency Python toolkit** of from-scratch building blocks — Go-style
`iota` enumerations, lazy sequence generators, and a Pike-VM regex engine.

```bash
pip install PythonIota
```

Requires Python 3.10+.

## 1.9.0

### New features

- Regex searches skip impossible starts using a conservative literal prefix;
  diagnostics expose the prefix and account for prefilter operations.
- CLI mutually exclusive groups, version output and static completion scripts for
  Bash, Zsh and PowerShell extend the existing metadata-only completion API.
- Sequence pipelines add bounded `limit`, grouping, Top-K, partitioning, iterator
  copies and combinatorics, with protection for known non-exhausting inputs.

### Fixes

- Pattern backreferences now fail explicitly instead of silently matching digits.
- Out-of-range regex positions are clamped to the text, matching `re` semantics.
- Counted nullable repetitions stop after zero-width iterations without losing
  the preferred non-empty alternative during iteration.

## 1.8.0

### New features

- Generic, single-pass `IotaIterator` pipelines support continuous chaining.
- CLI options support explicit JSON/mapping configuration, opt-in environment
  values and metadata-only completion candidates.
- Regex operations support shared step/time budgets and structured diagnostics.

### Fixes

- Mapping derived sequences preserves their lazy iteration, element types and
  finite/infinite behavior; `filter` still returns an iterator.
- Empty regex matches, conditional capture states and zero-width repetitions
  retain the appropriate alternatives and iteration order.
- Parent CLI positionals are validated before subcommand dispatch.
- Enum aliases cannot overwrite existing or reserved names; alias identifiers
  must already be NFKC-normalized to agree with Python attribute syntax.
- Sequence APIs carry generic element types, and distributions include `py.typed`.

## 1.7.1 fixes

- Full matching now considers complete alternatives instead of rejecting a
  valid match after selecting a shorter branch.
- Ordered enum members now hash consistently with equal numeric values and
  equal members of other enums, preserving dictionary and set lookups.

**What's inside**

- **Enums** — Go-style `iota`, bit flags, string enums (`pythoniota.IotaEnum`, ...)
- **Sequences** — lazy, chainable generators (`pythoniota.iota`)
- **Regex** — a from-scratch NFA (Pike VM) engine, no catastrophic backtracking (`pythoniota.regex`)
- **CLI** — a from-scratch `argparse`-style argument parser (`pythoniota.cli`)

---

## Enums

### Go-style `iota`

`iota` starts at `0` and auto-increments each time it is read. Literal assignments do **not** consume it.

```python
from pythoniota import IotaEnum

class Color(IotaEnum):
    Red   = iota   # 0
    Green = iota   # 1
    Blue  = iota   # 2

Color.Green          # 1
Color.names()        # ['Red', 'Green', 'Blue']
Color.values()       # [0, 1, 2]
list(Color)          # [('Red', 0), ('Green', 1), ('Blue', 2)]
0 in Color           # True (by value)
'Red' in Color       # True (by name)
Color.from_value(2)  # 'Blue'
```

`iota` works inside expressions — the value is the current counter:

```python
class Perm(IotaEnum):
    Read  = 1 << iota          # 1
    Write = 1 << iota          # 2
    Exec  = 1 << iota          # 4
    All   = Read | Write | Exec # 7

class Size(IotaEnum):
    KB = 1 << (iota + 10)      # 1024
    MB = 1 << (iota + 10)      # 2048
    GB = 1 << (iota + 10)      # 4096
```

### Skipping values & custom start/step

```python
class Errno(IotaEnum):
    EPERM = iota   # 0
    skip(2)        # skip 1, 2
    EBADF = iota   # 3

class Port(IotaEnum):
    _iota_start_ = 8000
    _iota_step_  = 10
    HTTP  = iota   # 8000
    HTTPS = iota   # 8010
```

`_ = iota` also skips a single value (Go's blank identifier).

### Immutability, aliases, serialization

```python
Color.Red = 99          # AttributeError: cannot modify enum member
Color.alias('R', 'Red') # Color.R == 0
Color.to_dict()         # {'Red': 0, 'Green': 1, 'Blue': 2}
Color.to_json()         # '{"Red": 0, "Green": 1, "Blue": 2}'
Color.from_json(s)      # -> dict of members
Color.has('Red')        # True
Color.get('Nope', -1)   # -1
```

Alias names must be public Python identifiers. Existing members, aliases,
methods and reserved/internal names cannot be overwritten; rejected aliases
leave the enum unchanged.

### `@unique` — reject duplicate values

```python
from pythoniota import unique

@unique
class Status(IotaEnum):
    Active   = iota
    Inactive = iota
# raises ValueError if two members share a value
```

### Ordered enums

Add `_ordered_ = True` to make members comparable by declaration order:

```python
class Priority(IotaEnum):
    _ordered_ = True
    Low    = iota
    Medium = iota
    High   = iota

Priority.Low < Priority.High   # True
```

### Bit flags

```python
from pythoniota import IotaBitFlags, FlagScope

class Access(IotaBitFlags):
    Read  = 1 << iota   # 1
    Write = 1 << iota   # 2
    Exec  = 1 << iota   # 4

flags = Access.Read | Access.Write
flags.has(Access.Read)              # True
flags.has_all(Access.Read, Access.Exec)  # False
flags.has_any(Access.Read, Access.Exec)  # True
list(flags.decompose())             # [BitFlag(1), BitFlag(2)]

with FlagScope() as scope:
    scope.grant(Access.Read, Access.Write)
    scope.revoke(Access.Write)
    scope.has(Access.Read)          # True
```

### String enums

Members resolve to their own name, or a custom format:

```python
from pythoniota import IotaStringEnum

class Color(IotaStringEnum):
    Red  = iota   # 'Red'
    Blue = iota   # 'Blue'

class Code(IotaStringEnum):
    _format_ = "{name}_{index:03d}"
    OK    = iota  # 'OK_000'
    Error = iota  # 'Error_001'
```

### `@iota_enum` decorator

Turn a plain class into an enum:

```python
from pythoniota import iota_enum

@iota_enum
class Color:
    Red = 0
    Green = 1
    Blue = 2

@iota_enum(ordered=True)
class Priority:
    Low = 0
    High = 1
```

### `match` / `case`

Enum members match by value:

```python
match color:
    case Color.Red:   ...
    case Color.Blue:  ...
    case _:           ...
```

---

## Sequences

`iota(...)` is a lazy, composable sequence. Construction mirrors `range`, plus an optional `map`:

```python
from pythoniota import iota

iota()                       # 0, 1, 2, ...        (infinite)
iota(5)                      # 0, 1, 2, 3, 4
iota(2, 10, 2)               # 2, 4, 6, 8
iota(5, map=lambda i: i*i)   # 0, 1, 4, 9, 16

s = iota(10)
len(s)          # 10        (O(1))
s[7]            # 7         (O(1) arithmetic indexing)
s[2:5]          # [2, 3, 4]
list(reversed(s))   # 9, 8, ... 0   (lazy)
5 in s          # True      (O(1))
s.take(3)       # [0, 1, 2]
```

### Operators

```python
iota(3) + iota(3, 6)   # concat: 0,1,2,3,4,5
iota(3) * 2            # repeat: 0,1,2,0,1,2
iota(3) | iota(3, 6)   # interleave: 0,3,1,4,2,5
iota(3) @ iota(3, 6)   # zip: (0,3),(1,4),(2,5)
```

### Lazy combinators

```python
iota(10).filter(lambda x: x % 2 == 0)   # 0,2,4,6,8
iota().map(lambda x: x*x)               # 0,1,4,9,...
iota().takewhile(lambda x: x < 5)       # 0,1,2,3,4
iota(10).dropwhile(lambda x: x < 7)     # 7,8,9
iota(4).pairwise()                      # (0,1),(1,2),(2,3)
iota(5).window(3)                       # (0,1,2),(1,2,3),(2,3,4)
iota(7).chunk(3)                        # [0,1,2],[3,4,5],[6]
iota(6).map(lambda x: x % 3).distinct() # 0,1,2
seq.flatten()                           # flatten one level
iota().enumerate()  .accumulate()  .zip(other)
```

### Chainable iterator pipelines

```python
from pythoniota import IotaIterator, iota

windows = iota(20).filter(lambda x: x % 2 == 0).map(str).window(3)
windows.take(2)  # [('0', '2', '4'), ('2', '4', '6')]
next(windows)   # ('4', '6', '8'): continues the same stream
IotaIterator([1, 2, 3]).map(lambda x: x * 10).to_list()  # [10, 20, 30]
iota().filter(lambda x: x % 2 == 0).map(str).take(3)    # ['0', '2', '4']
```

`filter`, `accumulate`, `enumerate`, `zip`, `chunk`, `window`, `takewhile`,
`dropwhile`, `pairwise`, `flatten` and `distinct` return `IotaIterator` pipelines,
which can continue chaining. They are lazy and single-pass (`iter(pipe) is pipe`),
with element types preserved through generic transformations. `take(n)` still
returns a list. Chunk/window sizes must be positive and are checked before consumption.

Base sequences and their `.map()` results remain reusable. Iterator pipelines
have no length, but expose `infinite`: `True` for known non-exhausting sources,
`False` for known finite sources, and `None` when unknown. Exhaustive terminals
reject known non-exhausting pipelines before consuming them. Filtering/distinct
can yield few values without ever exhausting their infinite input; these retain
the protection. Arbitrary generators and early-stopping `takewhile` may remain
unknown, so this is not a general termination detector.

Use `.limit(n)` to keep a bounded prefix chainable (`take(n)` still returns a list):

```python
iota().filter(lambda x: x % 2 == 0).limit(5).sum()  # 20
```

`limit` bounds the number of returned elements, not the time to produce one:
`iota().filter(lambda x: False).limit(1)` still cannot yield a value. Short-circuit
operations (`first`, `find`, `any`, `all`, `take`) remain available on infinite input.

### Grouping, selection and iterator utilities

```python
iota(6).group_by(lambda x: x % 2)  # {0: [0, 2, 4], 1: [1, 3, 5]}
iota(10).top_k(3)                 # [9, 8, 7]
iota(10).top_k(3, largest=False)  # [0, 1, 2]
iota(2).zip_longest(["a"], fillvalue="-").to_list()  # [(0, 'a'), (1, '-')]
iota(2).product(["a", "b"]).to_list()  # [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
iota(3).permutations(2).take(3)   # [(0, 1), (0, 2), (1, 0)]

odds, evens = iota(6).partition(lambda x: x % 2 == 0)
odds.to_list(), evens.to_list()  # ([1, 3, 5], [0, 2, 4])
left, right = iota(3).tee()
left.take(2), right.to_list()    # ([0, 1], [0, 1, 2])
```

`group_by(key=None)` groups globally into lists, preserving key/element insertion
order; unlike `itertools.groupby`, equal keys need not be adjacent. `top_k` supports
`key=`, preserves input order for ties and keeps O(k) selection storage, but must
consume the entire finite input. `top_k(0)` returns immediately without consuming.
Neither operation can compute a global result from an infinite stream.

`partition` returns false/true branches and evaluates its predicate once per item.
Like `tee`, it buffers when consumers advance at different rates; that buffer can
grow without bound. `product` and `permutations` defer input consumption until
iteration, then cache their inputs like `itertools`; they reject known infinite
inputs and are not constant-memory streaming operations.

### Terminal operations

Reductions on plain arithmetic sequences are O(1); known-infinite base sequences
reject exhaustive terminals. Iterator pipelines consume their remaining elements.

```python
iota(1, 101).sum()      # 5050   (closed-form, O(1))
iota(3, 10).min()       # 3
iota(3, 10).max()       # 9
iota(0, 10, 2).count()  # 5
iota(5).last()          # 4
iota(10).nth(3)         # 3
iota().first()          # 0
iota().find(lambda x: x > 100)   # 101
iota(1, 5).all(lambda x: x > 0)  # True
iota(5).any(lambda x: x == 3)    # True
iota(3).reduce(lambda a, b: a + b)  # 3

iota(3).to_list()                     # [0, 1, 2]
iota(3).to_tuple()                    # (0, 1, 2)
iota(3).to_set()                      # {0, 1, 2}
iota(3).to_dict(value=lambda x: x*x)  # {0: 0, 1: 1, 2: 4}
```

### Async iteration

```python
async for x in iota(5):
    ...
```

---

## Recipes

```python
from pythoniota.recipes import (
    fibonacci, lucas, factorial, catalan, harmonic,
    triangle, powers, geometric, primes, primes_sieve,
    pascal_row, collatz, repeat, cycle,
)

fibonacci(10).to_list()   # 0,1,1,2,3,5,8,13,21,34
lucas(7).to_list()        # 2,1,3,4,7,11,18
factorial(6).to_list()    # 1,1,2,6,24,120
catalan(6).to_list()      # 1,1,2,5,14,42
harmonic(4).to_list()     # 1.0, 1.5, 1.833..., 2.083...
triangle(5).to_list()     # 0,1,3,6,10
powers(2, 8).to_list()    # 1,2,4,...,128
geometric(3, 2, 4).to_list()  # 3,6,12,24
primes(5).to_list()       # 2,3,5,7,11        (first n primes)
primes_sieve(20).to_list()# 2,3,5,7,11,13,17,19  (< limit, Eratosthenes)
pascal_row(4).to_list()   # 1,4,6,4,1
collatz(6).to_list()      # 6,3,10,5,16,8,4,2,1
repeat('x', 3).to_list()  # ['x','x','x']
cycle([1,2], 5).to_list() # 1,2,1,2,1
```

Recipes without a count argument produce infinite sequences: `fibonacci().take(20)`, `primes().takewhile(lambda p: p < 100)`.

---

## Safe expression evaluation

`safe_eval` evaluates arithmetic expressions over a whitelisted AST (no `eval`, no names/calls beyond provided variables):

```python
from pythoniota import safe_eval

safe_eval("2 ** 10")                 # 1024
safe_eval("a * b + 1", {"a": 3, "b": 4})  # 13
```

---

## Regex

A from-scratch regular-expression engine that runs bytecode with an ordered
**Pike VM**. Plain patterns such as `(a+)+$` avoid catastrophic backtracking and
run in linear time in the text length for a fixed pattern. This is not a
universal complexity guarantee: lookaround can rescan suffixes, and conditionals
can retain multiple capture-participation states. Use explicit `max_steps` and
`timeout` limits when matching potentially expensive patterns.

```python
from pythoniota import regex

m = regex.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-08-02")
m.groups()                                   # ('2026', '08', '02')

regex.findall(r"\w+", "hello world")          # ['hello', 'world']
regex.sub(r"\s+", "_", "a  b   c")            # 'a_b_c'
regex.split(r"[,;]\s*", "a, b; c,d")          # ['a', 'b', 'c', 'd']

named = regex.search(r"(?P<user>\w+)@(?P<host>\w+)", "user@host")
named.groupdict()                             # {'user': 'user', 'host': 'host'}
```

Supported: literals, `.`, `* + ?` and their lazy forms `*? +? ??`, alternation
`|`, groups `(...)` / non-capturing `(?:...)` / named `(?P<name>...)`, lookahead
`(?=...)` / `(?!...)`, fixed-width lookbehind `(?<=...)` / `(?<!...)`, conditionals
`(?(id/name)yes|no)`, character classes `[...]`, counted repeats `{m,n}`,
shorthands `\d \w \s` (and `\D \W \S`), word boundaries `\b \B`, anchors
`^ $ \A \Z`, and hex escapes `\xHH` / `\uHHHH`. Flags: `IGNORECASE`, `MULTILINE`,
`DOTALL`, `VERBOSE` (also inline `(?imsx)`). API mirrors `re`: `compile`, `match`,
`fullmatch`, `search`, `findall`, `finditer`, `sub`, `subn`, `split`, `escape`,
with `Match.group` / `groups` / `span` / `groupdict` / `expand`.

```python
regex.findall(r"[a-z]+", "AbCdEf", regex.IGNORECASE)   # ['AbCdEf']
regex.findall(r"\d+(?=px)", "10px 20em 30px")           # ['10', '30']  (lookahead)
```

Pattern backreferences (`\1`, `(?P=name)`) raise `RegexError`; this is an NFA-based
engine rather than a general backtracking engine. Replacement templates still
support backreferences. Patterns are text strings, not bytes. Empty matches follow
the iteration and splitting behavior of Python 3.10–3.13 `re`, including a non-empty
match immediately after an empty one at the same position.

```python
regex.search(r"(a+)+$", "a" * 40 + "!")       # None, without backtracking
```

### Execution limits and diagnostics

```python
pattern = regex.compile(r"(?=a*$)")
try:
    result = pattern.search("a" * 100 + "!", max_steps=1000, timeout=0.1)
except regex.RegexLimitError as error:
    print(error.reason, error.steps)

structure = pattern.explain()  # JSON-serializable bytecode, groups and nested assertions
report = pattern.diagnose("aaaa!", max_steps=1000)
print(report["status"], report["steps"], report["op_counts"])
```

Search and iteration use a conservative mandatory literal prefix when available.
`explain()["literal_prefix"]` shows it; case-insensitive patterns and leading
branching/nullable patterns fall back to the normal VM. Existing higher-priority
threads keep running: the optimization does not restart the VM for each candidate.

All matching, iteration, splitting and replacement functions accept keyword-only
`max_steps` (nonnegative integer) and `timeout` (nonnegative seconds), defaulting
to no limits. A step is a bytecode analysis/visit or one native prefix lookup
(`PREFILTER`), including epsilon closure and consuming states; counts are
implementation-specific, not a portable time unit or a per-character work bound.
Nested lookarounds and all matches within one call share a single budget.
`finditer` starts its timeout when created, including pauses between yields.

Limit exhaustion raises `RegexLimitError`, never a false no-match result.
`diagnose` supports `match`, `fullmatch` and `search`, instead returning an explicit
`limit_exceeded` status with its reason, counters, peak pending threads and elapsed
time. `explain` and `diagnose` also exist as module-level functions.

Limits apply after pattern compilation. Timeouts are cooperative: they cannot
preempt Python replacement callbacks, compilation or a single long native
operation; elapsed time is checked again after callbacks. For hard isolation of
untrusted patterns/callbacks, use a separate process and external resource limits.

---

## CLI

A faithful subset of the standard library's `argparse`, built from scratch:

```python
from pythoniota import cli

p = cli.ArgumentParser(prog="greet", description="say hello")
p.add_argument("name")
p.add_argument("-n", "--times", type=int, default=1)
p.add_argument("-v", "--verbose", action="store_true")

args = p.parse_args(["world", "--times", "3", "-v"])
args.name, args.times, args.verbose      # ('world', 3, True)
```

Supported:

- positional & optional arguments, `nargs` (`N` / `?` / `*` / `+`)
- `type` / `choices` / `default` / `required`
- actions: `store`, `store_true`, `store_false`, `store_const`, `append`, `count`
- `--opt=value`, clustered short flags (`-abc`), the `--` end-of-options marker
- an auto-generated `-h/--help`, and sub-commands via `add_subparsers`

Bad input prints a usage line and raises `SystemExit(2)` — the same contract as
argparse, so it drops into scripts and tests unchanged. `parse_known_args`
returns `(namespace, extras)` instead of erroring on unknown tokens.

### Mutually exclusive options and version output

```python
p = cli.ArgumentParser(prog="convert")
mode = p.add_mutually_exclusive_group(required=True)
mode.add_argument("--json", action="store_true")
mode.add_argument("--text", action="store_true")
p.add_argument("--version", action="version", version="%(prog)s 1.0")
p.parse_args(["--json"])
```

Groups accept optional arguments only. A required group needs exactly one explicit
member; an optional group permits zero or one. CLI, environment and configuration
values count as explicit, even `False` or zero; defaults and caller `Namespace`
values do not. CLI overrides lower sources for the same destination, not another
group member: CLI `--json` plus config `text=true` is a conflict. Repeating the same
member is allowed. Group members cannot themselves be required or share a destination.

A valid version request prints to stdout and exits with status 0 without reading
configuration or converting unrelated values. It bypasses missing required options
but still respects argument consumption, subcommand boundaries and `--`.

### Sub-commands

```python
p = cli.ArgumentParser(prog="git")
sub = p.add_subparsers(dest="cmd")

add = sub.add_parser("add", help="stage files")
add.add_argument("path")
add.add_argument("-f", "--force", action="store_true")

rm = sub.add_parser("rm", help="remove files")
rm.add_argument("path")

args = p.parse_args(["add", "README.md", "-f"])
args.cmd, args.path, args.force          # ('add', 'README.md', True)
```

Parent positionals declared before `add_subparsers` are consumed and validated
before the command. Parent options may occur before the command; trailing
options are parsed only by the selected child. As with argparse, variable
`nargs` are greedy and command names are not special delimiters, so prefer
fixed-count parent positionals for predictable subcommand syntax.

### Configuration and environment values

```python
p = cli.ArgumentParser(config={"port": 8000, "mode": "fast"}, env_prefix="APP_")
p.add_argument("--port", type=int, default=3000)
p.add_argument("--mode", choices=["fast", "slow"])
p.add_argument("--token", env_var="SERVICE_TOKEN")
args = p.parse_args(["--port", "9000"])  # --port overrides APP_PORT and config
```

`config` accepts a mapping or a UTF-8 JSON-object path, read again on each parse.
Keys are this parser's optional-argument destinations, not option spellings;
unknown keys and malformed files are errors. Sources do not fill positionals.
Precedence is explicit CLI > enabled environment > config > argument default;
only the winning value is converted and checked against `choices`. Required
options may be supplied by config/environment. Explicit caller `Namespace`
values are retained unless the CLI changes them, but do not by themselves
satisfy required options.

Environment lookup is opt-in via `env_prefix` or an exact `env_var`; children
configure their own sources with `add_parser(config=..., env_prefix=...)`.
Scalar environment values are literal strings. Multi-value options use JSON
arrays; `append` uses an array of occurrences (nested arrays for multi-value
occurrences). Boolean values accept true/false, yes/no, on/off and 1/0; counts
must be nonnegative integers. Explicit CLI append/count replaces lower sources,
while legacy accumulation from argument defaults is retained when no external
source applies. No configuration contents are executed as code or shell text.

### Completion candidates

```python
p.complete([], "--po")             # ['--port']
p.complete(["--mode"], "s")         # ['slow']
p.complete([], "--mode=f")         # ['--mode=fast']
```

`complete(tokens, prefix="")` accepts finished words without the program/current
word, then returns sorted, unique full replacement words for options, choices
and subcommands. It does not parse, read config/environment, call type converters,
execute shells or mutate parse state. Variable-length positionals are best-effort;
only built-in collections of plain scalar choices are enumerated, not generators
or custom objects. This is an adapter API for shells/editors: it does not install
shell hooks, quote candidates or perform filesystem completion.

### Static shell completion scripts

```python
script = p.completion_script("bash", command="convert")
# Also accepts "zsh" and "powershell"; save/load the returned text yourself.
```

This generates a quoted static snapshot of options, subcommands and finite scalar
choices. It does not run the application, read sources, call converters or change
shell configuration. Unlike `complete(tokens, prefix)`, the snapshot is not a
context-sensitive parser: it may suggest candidates from other options or commands.
Regenerate it after changing parser metadata. Shell metacharacters are emitted as
literal data, not executable fragments. Command names containing control characters
are rejected; such candidates are omitted. Zsh requires an initialized completion
system (`compinit`).

---

## License

MIT
