Skip to content

modern_decode: read abstract sequence interfaces as several values - #36

Merged
thorwhalen merged 2 commits into
masterfrom
abstract-sequence-hints
Sep 3, 2026
Merged

thorwhalen merged 2 commits into
masterfrom
abstract-sequence-hints

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

The problem

argh recognises list and nothing else, so a parameter annotated names: Sequence[str] became one positional taking one token:

$ tool check scale eq lut3d
usage: tool [-h] {check,...} ...
tool: error: unrecognized arguments: eq lut3d

The failure is quiet in the way that costs time: the parser builds, --help looks right, the command runs, and the error points at the value rather than at the annotation. And it penalises exactly the annotation style that prefers collections.abc interfaces over concrete containers — Sequence[str] means the same thing to a reader as list[str], and meant something else to the parser.

Measured before the change:

annotation variadic?
list[str], List[str], list ✅
Sequence[str], Iterable[str], tuple[str, ...], Sequence ❌

The change

modern_decode now covers Sequence / MutableSequence / Iterable / Collection / Set / MutableSet, plus tuple / set / frozenset, subscripted or bare.

argh_decode is untouched. It is argh's if-chain if-branch for if-branch, and widening it would break the compatibility contract that is its whole purpose — modern_decode is the seam for exactly this, and its fall-through composition made the change three lines. A test asserts argh_decode still returns {} for every new spelling and still returns what it always did for list[int].

Three decisions

  • str and bytes must never be read as sequences, and this avoids it by construction rather than by a special case: matching is on typing.get_origin, and get_origin(str) is None. Both are registered Sequences, so an issubclass test would have read every string parameter as variadic and broken every existing CLI. Tested directly, because it is the trap this change lives inside.
  • A homogeneous fixed-length tuple gets a fixed nargs — tuple[int, int] → {'nargs': 2, 'type': int}. It is the one case where the annotation carries a count, and it makes --size W H fall out of the signature.
  • A heterogeneous tuple gets no inference at all. add_argument has a single type and there is no honest one to choose, so tuple[int, str] → {} rather than guessing one and converting the other wrongly.

Tests

952 passing (was 932). 20 new: the abstract interfaces subscripted and bare, the str/bytes trap, both tuple cases, Optional[Sequence[int]], and the argh_decode-is-unchanged guard.

Second commit is pure formatting of two test files ruff format had drifted from — split out so the functional commit contains only the functional change.

Where it came from

Adopting cw as the CLI for looks, whose house style annotates with collections.abc interfaces throughout. cw was the right package — MIT, dependencies = [], and import cw verified to pull in nothing beyond stdlib — it just could not read the annotations the calling code was already written with.

https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX

`argh` recognises `list` and nothing else, so a parameter annotated
`names: Sequence[str]` became ONE positional taking ONE token. The failure is
quiet in the way that costs time: the parser builds, `--help` looks right, the
command runs, and the second argument comes back as an "unrecognized argument"
pointing at the value rather than at the annotation.

That penalises exactly the annotation style that prefers `collections.abc`
interfaces over concrete containers — a signature written `Sequence[str]` means
the same thing to a reader as `list[str]` and meant something else to the
parser.

`modern_decode` now covers `Sequence` / `MutableSequence` / `Iterable` /
`Collection` / `Set` / `MutableSet`, plus `tuple` / `set` / `frozenset`,
subscripted or bare. `argh_decode` is **untouched**: it is argh's if-chain
if-branch for if-branch, and widening it would break the compatibility contract
that is its whole purpose. A test asserts it still returns `{}` for every new
spelling, and still returns what it always did for `list[int]`.

Three decisions in the implementation:

- **`str` and `bytes` must never be read as sequences**, and matching on
  `typing.get_origin` avoids it BY CONSTRUCTION rather than by a special case:
  `get_origin(str)` is None. Both are registered `Sequence`s, so an `issubclass`
  test would have read every string parameter as variadic and broken every
  existing CLI. Tested directly, because it is the trap this change exists
  inside.
- **A homogeneous fixed-length tuple gets a fixed `nargs`** — `tuple[int, int]`
  -> `nargs=2, type=int`. It is the one case where the annotation carries a
  count, and it makes `--size W H` fall out of the signature.
- **A heterogeneous tuple gets no inference at all.** `add_argument` has a
  single `type` and there is no honest one to choose, so `tuple[int, str]`
  falls through to `{}` rather than guessing one and converting the other
  wrongly.

Found while adopting `cw` as the CLI for `looks` (t/looks), whose house style
annotates with `collections.abc` interfaces throughout.

Claude-Session: https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX
Pure line-wrapping and quote normalisation, no semantics. They were already
unformatted before this branch; `ruff format` picked them up while formatting
the grammar change, and they are split out here so the functional commit
contains only the functional change.

Claude-Session: https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX
thorwhalen added a commit to thorwhalen/looks that referenced this pull request Sep 2, 2026
Both owner questions ratified 2026-09-02. RULE G: `burns` keeps authored
geometry and gains a `looks`-backed ffmpeg backend, so `looks` owns the compile
side and the dependency points burns -> looks. RULE N: `looks` owns
normalisation, as already shipped in `Look.target`.

`looks.motion` is RULE G's compile half — keyframes in, an ffmpeg fragment out,
no easing and no `burns` import. It picks the filter from what the path does,
which is not a matter of taste: a recorded fleet fact said to use `crop` and
avoid `zoompan`, and measurement overturned it. Both of that advice's premises
are true (`t` is undefined; the default `d` duplicates frames) and its
conclusion is wrong (`in_time` works; `d=1` is exactly 1:1) — while `crop`
cannot express a zoom at all, refusing to configure rather than freezing. Three
further traps are silent wrong answers and are now refusals: x/y are in original
input pixels (60.5 dB vs 6.3), `fps` retimes without changing the frame count
(2.0s -> 0.8s), and `zoom` is clamped at 10 (54.4 dB vs 13.2). The tests compare
pictures with ffmpeg's own psnr, because string equality passes for a fragment
ffmpeg refuses and equally for one that renders the wrong thing.

The CLI dispatches through `cw` (MIT, no dependencies, stdlib-only on import)
rather than the LGPL package or hand-rolled argparse, with the Sequence-reading
fix contributed as i2mint/cw#36.

Two of the package's own guards caught this work: the extras ledger refused the
new `[cli]` extra until `cw` had a row, and the pin over the decisions table
refused a silently-grown ledger. A third had been reporting "skipped" on half of
CI since it was written, for want of `tomllib` on 3.10 — now read through a
small reader that is checked against `tomllib` on the leg that has one.

628 tests on 3.12, 545 on 3.10. Follow-ups filed: thorwhalen/muvid#68 (the
docstring at its source), thorwhalen/burns#12 (the adapter).
@thorwhalen
thorwhalen merged commit b00e95a into master Sep 3, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the abstract-sequence-hints branch September 3, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant