Skip to content

Fix and decompose the command line interface - #19

Merged
ESultanik merged 1 commit into
masterfrom
cli-fixes
Aug 7, 2026
Merged

Fix and decompose the command line interface#19
ESultanik merged 1 commit into
masterfrom
cli-fixes

Conversation

@ESultanik

Copy link
Copy Markdown
Owner

main() was a single 165-line function mixing argument definition, logging setup, three subcommands, and handle cleanup. It is now build_parser() (with three focused _add_*_arguments helpers), configure_logging, and one do_encrypt / do_decrypt / do_test / do_version per action. Every function is under 50 lines and within the branch limits.

Defects fixed

  • -5/--best was silently identical to -4. The five levels were separate store_true flags with -4 defaulting to True, so the dispatching elif chain reached -4 before it could consider -5, making 16-nibble-grams unreachable. One store_const destination makes every level distinct:

    -1 -> (1,)              -4 -> (1, 2, 4, 8)
    -2 -> (1, 2)            -5 -> (1, 2, 4, 8, 16)   <-- previously identical to -4
    -3 -> (1, 2, 4)
    
  • -t reported every byte combination as missing. It probed tuple((c,) for c in combination) — a tuple of int-tuples — against an alphabet keyed by tuples of bytes, so membership could never match. It also rendered results with chr() of values 0–15, i.e. control characters, and accumulated the message by repeated string concatenation. Now probes the right shape, renders hex nibbles, and caps the listing at 64 entries with a count of the remainder (four secrets means 65,536 of them).

  • Non-gzipped ciphertexts raised an uncaught BadGzipFile traceback. The decrypt path now uses auto_unzip, which accepts either, and - for stdin works as the help has always claimed.

  • --same-length / --length-checksum / --dictionary were three overlapping booleans, one of which defaulted to True and was never read. Now one store_const mode.

  • exit(main()) becomes sys.exit(main()); exit comes from site and is absent under -S.

  • LenticryptError is caught and logged as a message rather than a traceback.

argparse.FileType removed

Replaced by taking paths and opening them after parsing. FileType is deprecated — both ty and Python warn it "may leave files open" — because it opens eagerly during parsing, so a file could be opened and leaked when a later argument failed to validate. Opening in the action that uses the file, under an ExitStack, removes both the deprecation and the handle-tracking bookkeeping. This clears 135 PendingDeprecationWarnings from the test run.

Help text

Three stale strings corrected: -e cited "the -l argument" for truncation when it meant --same-length (-l is --log-level), --dictionary cited a nonexistent -c option, and -t claimed its report goes to stdout when it goes to the logger on stderr. --dictionary's claim about reducing ciphertext size is now qualified — measured, it only wins above roughly 8 KiB of plaintext.

Verification

Two xfail markers removed. Both had tested replicas of the buggy logic rather than the real code, so they were retargeted at build_parser and missing_combinations before being unmarked. 13 new CLI tests cover mode and level selection, every compression level round-tripping end to end, and both gzipped and plain ciphertexts.

173 passed / 3 xfailed. ruff and ty clean on __main__.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy

`main()` was a single 165-line function mixing argument definition, logging
setup, three subcommands and handle cleanup. It is now `build_parser()` (with
three focused `_add_*_arguments` helpers), `configure_logging`, and one
`do_encrypt` / `do_decrypt` / `do_test` / `do_version` per action. Every function
is under 50 lines and within the branch limits.

Defects fixed along the way:

  * **`-5/--best` was silently identical to `-4`.** The five levels were separate
    `store_true` flags with `-4` defaulting to True, so the dispatching `elif`
    chain reached `-4` before it could consider `-5`, and 16-nibble-grams were
    unreachable. One `store_const` destination makes every level distinct.
  * **`-t` reported every byte combination as missing.** It probed
    `tuple((c,) for c in combination)` -- a tuple of int-tuples -- against an
    alphabet keyed by tuples of `bytes`, so membership could never match. It also
    rendered results with `chr()` of values 0-15, i.e. control characters, and
    accumulated the message by repeated string concatenation. Now probes the
    right shape, renders hex nibbles, and caps the listing at 64 entries with a
    count of the remainder (four secrets means 65,536 of them).
  * **Non-gzipped ciphertexts raised an uncaught `BadGzipFile` traceback.** The
    decrypt path now uses `auto_unzip`, which accepts either, and `-` for stdin
    works as the help has always claimed.
  * `--same-length` / `--length-checksum` / `--dictionary` were three overlapping
    booleans, one of which defaulted to True and was never read. Now one
    `store_const` mode.
  * `exit(main())` becomes `sys.exit(main())`; `exit` comes from `site` and is
    absent under `-S`.
  * `LenticryptError` is caught and logged as a message rather than a traceback.

`argparse.FileType` is replaced by taking paths and opening them after parsing.
It is deprecated -- `ty` and Python both warn that it "may leave files open" --
because it opens eagerly during parsing, so a file could be opened and leaked
when a later argument failed to validate. Opening in the action that uses the
file, under an ExitStack, removes both the deprecation and the handle-tracking
bookkeeping. This clears 135 PendingDeprecationWarnings from the test run.

Three stale help strings corrected: `-e` cited "the `-l` argument" for truncation
when it meant `--same-length` (`-l` is `--log-level`), `--dictionary` cited a
nonexistent `-c` option, and `-t` claimed its report goes to stdout when it goes
to the logger on stderr. `--dictionary`'s claim about reducing ciphertext size is
now qualified: measured, it only wins above roughly 8 KiB of plaintext.

Two xfail markers are removed. Both had tested *replicas* of the buggy logic
rather than the real code, so they were retargeted at `build_parser` and
`missing_combinations` before being unmarked. 13 new CLI tests cover mode and
level selection, every compression level round-tripping end to end, and both
gzipped and plain ciphertexts.

`ruff` and `ty` are clean on `__main__.py`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy
@ESultanik
ESultanik merged commit d28474b into master Aug 7, 2026
11 checks passed
@ESultanik
ESultanik deleted the cli-fixes branch August 7, 2026 20:00
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