bindery-cli 0.47.1__tar.gz → 0.47.2__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.
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/CLAUDE.md +3 -1
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/PKG-INFO +9 -1
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/README.md +8 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/patchnotes.md +26 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/pyproject.toml +1 -1
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/cli.py +79 -29
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_cli.py +48 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/uv.lock +1 -1
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/.github/FUNDING.yml +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/.gitignore +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/AGENTS.md +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/LICENSE +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/logo.svg +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/plugin/__init__.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/project.done +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/roadmap.md +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/run_tests.sh +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/scripts/build_plugin.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/scripts/fast_sweep.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/spec.md +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/audit.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/epub.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/library.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/transforms.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/validate.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_audit.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_comment_hyphens.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_cover_tocdrift.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_epub.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_flags_real_core.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_flags_wiring.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_library.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_matcher_hardening.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_pagination.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_plugin.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_space_renames.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_transforms.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_validate.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_version.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_watermark.py +0 -0
- {bindery_cli-0.47.1 → bindery_cli-0.47.2}/tests/test_xml_hardening.py +0 -0
|
@@ -118,7 +118,9 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
118
118
|
- `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan). Since v0.46.0 the opt-in selection is the `RepairFlags` dataclass (one field per flag, all off by default; the all-off instance IS the default pass): `repair_epub(src, dst, flags)` and `process_book(..., flags)` take one object instead of a ~25-kwarg signature, the EPUB2-targeted fixes' version gate swaps in a `dataclasses.replace` copy (never mutating the caller's instance), and the plugin's bare `repair_epub(src, dst)` call constructs exactly the all-off default pass. Since v0.38.0/v0.39.0 (Phase 16): `generate_container` + the `--fix-container` gateway (insert after mimetype when missing, replace in-loop when stale; constant-epoch timestamp keeps bytes deterministic), `prune_dangling_edges` (prune_missing_manifest_items returns the pruned ids and the edges that pointed at them (spine@toc, media-overlay, fallback, EPUB2 cover meta) are rewritten so pruning cannot manufacture the regression the gate rejects), and `fix_manifest_media_types` (extension map + magic-byte confirmation via a `peek` callable over the open source zip; jpg/png/gif only), and `fix_cover_meta` (the ruling's deterministic half: dangling EPUB2 cover meta re-pointed from the guide's cover reference when exactly one manifest item carries that file, removed otherwise; accepted under no_worse like the lossy strips because cover wiring is invisible to epubcheck).
|
|
119
119
|
- `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
|
|
120
120
|
- `src/bindery/library.py`: Calibre walk, atomic replace, backups, and native format installation. Since v0.19.0 `CalibreIdResolver` resolves the book id from `metadata.db` through cquarry (one lazy path→id map per run; since v0.20.0 the map comes from `CalibreDB.format_path_index()`, re-normalized with `resolve().lower()` for the resolver's symlinked-directory and case-insensitive matching); the `(id)` directory-name regex is only the no-catalog fallback. Since v0.24.0 `install_format()` places the repaired file atomically and updates the `data` row through `cquarry.write.WritableCalibreDB` (since v0.31.0 via `set_format`, cquarry 1.17's sanctioned remove+add in one transaction); the external `calibredb` CLI is gone. Since v0.32.0 a guessed id drives a row update only when metadata.db corroborates it (`_verify_guess`: books row exists, file in the book's own directory, catalogued `data.name` when a row exists); anything else saves in place and leaves the catalog untouched: never reintroduce directory-name guessing as the primary source: renamed/mismatched directories would replace the wrong book. Since 2026-09-15 `make_backup` takes the opt-in `--backup-keep N` ring: at most N backup files per book, the author original `.bak` never deleted, newest content at the highest name; default remains an unbounded rotation.
|
|
121
|
-
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; `repair --json` emits the library per-book record vocabulary (2026-09-15; since v0.45.0 every per-book record, the phase1 payload included, also carries the structured fix breakdown: the `fixes` dict plus `ncx_uid_synced`/`watermark_refusals` off the `Outcome`, and `_phase1_decisions` reads that data instead of substring-matching the rendered `summary`, which stays for human eyes; CalibreQuarry's lossy-consent mirror is the waiting consumer, floor `bindery>=0.45.0`); since v0.29.0 also the `run` verbs: `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2; since 2026-09-15 the summary sums applied/equal states only and carries rejected projections as `rejected_projection`). Plus `bindery doctor` (2026-09-15): the environment self-check that imports none of the VirInvictus stack, never raises, and always exits 0. Both run verbs take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps. Since v0.46.0 `cli._flags_from_args(args)` is the one place the argparse names map onto `RepairFlags` fields (each `or --all`), shared by `run_library` and `run_repair`; the old per-verb ~25-kwarg blocks are gone. Since v0.47.1 the help is two-level
|
|
121
|
+
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; `repair --json` emits the library per-book record vocabulary (2026-09-15; since v0.45.0 every per-book record, the phase1 payload included, also carries the structured fix breakdown: the `fixes` dict plus `ncx_uid_synced`/`watermark_refusals` off the `Outcome`, and `_phase1_decisions` reads that data instead of substring-matching the rendered `summary`, which stays for human eyes; CalibreQuarry's lossy-consent mirror is the waiting consumer, floor `bindery>=0.45.0`); since v0.29.0 also the `run` verbs: `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2; since 2026-09-15 the summary sums applied/equal states only and carries rejected projections as `rejected_projection`). Plus `bindery doctor` (2026-09-15): the environment self-check that imports none of the VirInvictus stack, never raises, and always exits 0. Both run verbs take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps. Since v0.46.0 `cli._flags_from_args(args)` is the one place the argparse names map onto `RepairFlags` fields (each `or --all`), shared by `run_library` and `run_repair`; the old per-verb ~25-kwarg blocks are gone. Since v0.47.1 the help is two-level (the --help-repairs reference is
|
|
122
|
+
custom-rendered and colors itself with the CPython 3.14 argparse theme,
|
|
123
|
+
gated by argparse's own _colorize detection so the levels always agree) and the repair-flag inventory is one table: `_REPAIR_FLAG_TABLE` (dest + one-line short help + long reference text) registers the compact grouped `--help` (argparse argument groups, hand-short usage line) and `--help-repairs` renders the long descriptions (a custom argparse Action, deliberately NOT store_true so the wiring test's selection count ignores it); adding a flag means adding a table row. `library --workers N` (v0.30.0) parallelizes only the sweep's candidate pass, in windows of N consumed in order so `--limit` stays lazy; the repair phase is never parallel (shared workdir, atomic-replacement contract). Since v0.40.0 `main()` guards the VirInvictus stack: a `ModuleNotFoundError` naming `vir_tui`/`cquarry` (marker-gated to 3.14+ on the 3.12 floor) becomes one stderr line and trouble exit 2 instead of a traceback; anything else missing still raises.
|
|
122
124
|
- `plugin/__init__.py` + `scripts/build_plugin.py`: the Bindery Repair Calibre plugin (v0.37.0; identity `Bindery Repair` / `bindery_repair`, approved and built per the scoped spec in roadmap.md Phase 3). The builder vendors `transforms.py`, `epub.py`, `pagination.py`, `watermark.py`, `reserialize.py` byte-identically into the zip (the zip root is a package, so their relative imports resolve unchanged; the suite's drift test pins the equality) with the version tuple substituted from the single-source VERSION; `publish.yml` attaches `BinderyRepair-v<VERSION>.zip` to each release. The plugin runs ONLY the default pass (epubcheck cannot gate inside Calibre), never raises, is byte-idempotent (zero fixes returns the original path), testzip()-verifies its output, logs one line per book, and takes JSON config via `site_customization` (`log`, `log_path`, `max_size_mb` default 150, `max_log_mb` rotation cap default 2, `epubcheck_path` experimental validation default OFF). Load it in tests through the `tests/test_plugin.py` stubs: the calibre namespace is installed in sys.modules and the package loads with a two-dir `__path__` (plugin dir first, then src/bindery) exactly like calibre's zipplugin loader shape. **Plugin/interpreter compatibility (v0.40.0):** v0.39.0 shipped PEP 758 unparenthesized except tuples, and the plugin died with a bare SyntaxError on every released Calibre (7.x and the whole 8.x series embed Python 3.11, 9.0+ embeds 3.14; from calibre's bypy sources.json, checked against the upstream clone). The core is now grammar-pinned: parenthesized except tuples everywhere, `ruff`'s `target-version = "py312"` pin (inference from requires-python would rewrite the parens back off), `tests/test_version.py`'s parse guard, and CI's `plugin-compat` job byte-compiling the built zip under 3.11/3.12/3.13/3.14. `minimum_calibre_version` is (7, 0, 0): the oldest series whose interpreter CI covers, replacing the hollow (2, 0, 0); `supported_platforms` stays `["linux"]` (developed and tested on Linux only). Any new syntax in the vendored modules must clear the guard before it ships.
|
|
123
125
|
- `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic/completeness, archive integrity, spine classification), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, the version pin and the syntax-floor grammar guard, and the Calibre plugin (build drift + run behavior under calibre stubs). Since the v0.46.0 follow-ups: `tests/test_flags_wiring.py` reflectively pins the RepairFlags inventory to the argparse dests and `_flags_from_args` mapping (a flag added without a field, a field without a flag, or a remapped line fails there), the all-off defaults, and the version gate's caller-immutability contract (test_epub's `TestVersionGateKeepsFlagsIntact`); `tests/test_comment_hyphens.py` carries the RSC-016 fix; `tests/test_flags_real_core.py` drives every repair flag from argv through the unmocked core (--no-validate; the gate itself stays pinned by the mocked-oracle tests and runs for real wherever epubcheck exists). All three ride CI's core-compat 3.12/3.13 leg; the hand-listed module list there must gain an entry for every new stack-free test module.
|
|
124
126
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bindery-cli
|
|
3
|
-
Version: 0.47.
|
|
3
|
+
Version: 0.47.2
|
|
4
4
|
Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/VirInvictus/bindery-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/VirInvictus/bindery-cli
|
|
@@ -203,6 +203,14 @@ vetting, phase-3 scoped post-import repair) for a calling agent, and `bindery do
|
|
|
203
203
|
checks the installation (see Install).
|
|
204
204
|
The sections below take each in turn.
|
|
205
205
|
|
|
206
|
+
Help is two-level: `bindery repair --help` (or `library --help`) lists the
|
|
207
|
+
shared repair flags grouped with one-line descriptions, and
|
|
208
|
+
`bindery repair --help-repairs` prints the full reference carrying every
|
|
209
|
+
flag's long rationale. On a terminal both are ANSI-colored with the same
|
|
210
|
+
theme Python 3.14's argparse uses; piped output stays plain, and `NO_COLOR`,
|
|
211
|
+
`FORCE_COLOR`, and `TERM=dumb` are honored exactly as `python --help` honors
|
|
212
|
+
them.
|
|
213
|
+
|
|
206
214
|
## Auditing
|
|
207
215
|
|
|
208
216
|
bindery-cli's auditing tool inspects EPUB body text for non-schema flaws that epubcheck cannot catch. It extracts and analyzes the visible text to detect content issues, producing console reports that can be used to filter your library or feed into `bindery repair`.
|
|
@@ -176,6 +176,14 @@ vetting, phase-3 scoped post-import repair) for a calling agent, and `bindery do
|
|
|
176
176
|
checks the installation (see Install).
|
|
177
177
|
The sections below take each in turn.
|
|
178
178
|
|
|
179
|
+
Help is two-level: `bindery repair --help` (or `library --help`) lists the
|
|
180
|
+
shared repair flags grouped with one-line descriptions, and
|
|
181
|
+
`bindery repair --help-repairs` prints the full reference carrying every
|
|
182
|
+
flag's long rationale. On a terminal both are ANSI-colored with the same
|
|
183
|
+
theme Python 3.14's argparse uses; piped output stays plain, and `NO_COLOR`,
|
|
184
|
+
`FORCE_COLOR`, and `TERM=dumb` are honored exactly as `python --help` honors
|
|
185
|
+
them.
|
|
186
|
+
|
|
179
187
|
## Auditing
|
|
180
188
|
|
|
181
189
|
bindery-cli's auditing tool inspects EPUB body text for non-schema flaws that epubcheck cannot catch. It extracts and analyzes the visible text to detect content issues, producing console reports that can be used to filter your library or feed into `bindery repair`.
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# bindery-cli Patch Notes
|
|
2
2
|
|
|
3
|
+
## v0.47.2 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
### The --help-repairs reference keeps argparse's color
|
|
6
|
+
|
|
7
|
+
The lattice-music lesson, landed here the same day: Python 3.14's argparse
|
|
8
|
+
colors `-h` on terminals by itself, so the argparse-rendered surfaces (the
|
|
9
|
+
top-level help and `-h` for every verb) kept their theme through the
|
|
10
|
+
v0.47.1 restructure untouched. The one gap was the page v0.47.1 added: the
|
|
11
|
+
custom-rendered `--help-repairs` reference printed plain, silently losing
|
|
12
|
+
the colorization every other surface has (the exact regression
|
|
13
|
+
lattice-music repaired in its 6b88a52).
|
|
14
|
+
|
|
15
|
+
* The reference now paints with the exact CPython 3.14 default theme
|
|
16
|
+
(bold blue headings, magenta prog, bold cyan long options; read off real
|
|
17
|
+
3.14 output, so the page reads like the argparse-rendered `-h` beside
|
|
18
|
+
it) through a single layout path whose disabled style emits empty codes.
|
|
19
|
+
* The color gate IS argparse's own detection (`_colorize.can_colorize` on
|
|
20
|
+
3.14+; plain False below 3.14, where argparse has no colorization to
|
|
21
|
+
match), so the two help levels can never disagree: verified under a pty
|
|
22
|
+
that `--help-repairs` carries the same code vocabulary as `-h` and that
|
|
23
|
+
`TERM=dumb` and `NO_COLOR` silence both, and that piped output stays
|
|
24
|
+
plain for README embedding and tests.
|
|
25
|
+
* Four new tests: the theme codes, the strip invariant (colored, codes
|
|
26
|
+
stripped == plain, byte for byte), pipe safety, and gate tracking.
|
|
27
|
+
TestHelpLayout is now nine; suite: 624.
|
|
28
|
+
|
|
3
29
|
## v0.47.1 (2026-10-06)
|
|
4
30
|
|
|
5
31
|
### The two-level help: compact grouped --help, full --help-repairs reference
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindery-cli"
|
|
7
|
-
version = "0.47.
|
|
7
|
+
version = "0.47.2"
|
|
8
8
|
description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
@@ -11,7 +11,7 @@ Calibre plugin built from this core.
|
|
|
11
11
|
Single source of truth for the version. pyproject.toml must match VERSION below.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "0.47.
|
|
14
|
+
VERSION = "0.47.2"
|
|
15
15
|
__version__ = VERSION
|
|
16
16
|
|
|
17
17
|
from .epub import RepairReport, repair_epub # noqa: E402
|
|
@@ -1642,6 +1642,84 @@ _REPAIR_FLAG_TABLE: dict[str, list[tuple[str, str, str]]] = {
|
|
|
1642
1642
|
}
|
|
1643
1643
|
|
|
1644
1644
|
|
|
1645
|
+
# The --help-repairs page is custom-rendered (not argparse output), so it
|
|
1646
|
+
# paints itself with the exact CPython 3.14 argparse default theme
|
|
1647
|
+
# (ColorfulTheme: bold blue headings, magenta prog, bold cyan long options
|
|
1648
|
+
# -- read off real 3.14 output, so the page reads like the argparse-rendered
|
|
1649
|
+
# -h beside it). This is the lattice-music lesson (6b88a52 there): a custom
|
|
1650
|
+
# help renderer that prints plain silently loses the colorization argparse
|
|
1651
|
+
# gives every other surface.
|
|
1652
|
+
_THEME = {
|
|
1653
|
+
"heading": "\x1b[1;34m",
|
|
1654
|
+
"prog": "\x1b[35m",
|
|
1655
|
+
"long_option": "\x1b[1;36m",
|
|
1656
|
+
"reset": "\x1b[0m",
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
def _can_color_help() -> bool:
|
|
1661
|
+
"""True exactly when argparse itself would colorize -h here: CPython
|
|
1662
|
+
3.14's own detection (_colorize.can_colorize: PYTHON_COLORS, NO_COLOR,
|
|
1663
|
+
FORCE_COLOR, TERM=dumb, isatty), so the two help levels can never
|
|
1664
|
+
disagree about color. Plain False below 3.14, where argparse has no
|
|
1665
|
+
colorization to match."""
|
|
1666
|
+
if sys.version_info < (3, 14):
|
|
1667
|
+
return False
|
|
1668
|
+
from _colorize import can_colorize
|
|
1669
|
+
|
|
1670
|
+
return can_colorize()
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _repair_flag_reference(color: bool | None = None) -> str:
|
|
1674
|
+
"""The full repair-flag reference: the _REPAIR_FLAG_TABLE rows with
|
|
1675
|
+
their long descriptions. `color=None` decides by the same gate argparse
|
|
1676
|
+
uses; the disabled style emits empty codes, so one layout path serves
|
|
1677
|
+
both faces and the strip invariant (colored, codes stripped == plain)
|
|
1678
|
+
holds by construction."""
|
|
1679
|
+
if color is None:
|
|
1680
|
+
color = _can_color_help()
|
|
1681
|
+
if color:
|
|
1682
|
+
heading, prog, option, reset = (
|
|
1683
|
+
_THEME["heading"],
|
|
1684
|
+
_THEME["prog"],
|
|
1685
|
+
_THEME["long_option"],
|
|
1686
|
+
_THEME["reset"],
|
|
1687
|
+
)
|
|
1688
|
+
else:
|
|
1689
|
+
heading = prog = option = reset = ""
|
|
1690
|
+
lines = [
|
|
1691
|
+
prog
|
|
1692
|
+
+ "bindery"
|
|
1693
|
+
+ reset
|
|
1694
|
+
+ " repair-flag reference (shared by `bindery repair` and "
|
|
1695
|
+
"`bindery library`)",
|
|
1696
|
+
"",
|
|
1697
|
+
"The always-on core pass is exactly five well-formedness fixes, the NCX",
|
|
1698
|
+
"pipeline, and the mimetype fix. Everything below is opt-in, and every",
|
|
1699
|
+
"repair is epubcheck-gated: applied only when the measured result improved",
|
|
1700
|
+
"(lossy strips: only when it did not get worse). Library mode is dry-run",
|
|
1701
|
+
"by default; `--all` enables every flag below at once. The README and",
|
|
1702
|
+
"spec.md carry the full rationale for each repair.",
|
|
1703
|
+
]
|
|
1704
|
+
for title, rows in _REPAIR_FLAG_TABLE.items():
|
|
1705
|
+
lines.append("")
|
|
1706
|
+
lines.append(heading + title + reset)
|
|
1707
|
+
lines.append("")
|
|
1708
|
+
for dest, _short, long_help in rows:
|
|
1709
|
+
lines.append(" " + option + "--" + dest.replace("_", "-") + reset)
|
|
1710
|
+
for para in long_help.split("\n"):
|
|
1711
|
+
lines.extend(
|
|
1712
|
+
textwrap.wrap(
|
|
1713
|
+
para,
|
|
1714
|
+
width=88,
|
|
1715
|
+
initial_indent=" ",
|
|
1716
|
+
subsequent_indent=" ",
|
|
1717
|
+
)
|
|
1718
|
+
or [" "]
|
|
1719
|
+
)
|
|
1720
|
+
return "\n".join(lines)
|
|
1721
|
+
|
|
1722
|
+
|
|
1645
1723
|
class _HelpRepairsAction(argparse.Action):
|
|
1646
1724
|
"""The second help level: print the full repair-flag reference and exit.
|
|
1647
1725
|
|
|
@@ -1665,35 +1743,7 @@ class _HelpRepairsAction(argparse.Action):
|
|
|
1665
1743
|
)
|
|
1666
1744
|
|
|
1667
1745
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
1668
|
-
|
|
1669
|
-
"bindery repair-flag reference (shared by `bindery repair` and "
|
|
1670
|
-
"`bindery library`)",
|
|
1671
|
-
"",
|
|
1672
|
-
"The always-on core pass is exactly five well-formedness fixes, the NCX",
|
|
1673
|
-
"pipeline, and the mimetype fix. Everything below is opt-in, and every",
|
|
1674
|
-
"repair is epubcheck-gated: applied only when the measured result improved",
|
|
1675
|
-
"(lossy strips: only when it did not get worse). Library mode is dry-run",
|
|
1676
|
-
"by default; `--all` enables every flag below at once. The README and",
|
|
1677
|
-
"spec.md carry the full rationale for each repair.",
|
|
1678
|
-
]
|
|
1679
|
-
for title, rows in _REPAIR_FLAG_TABLE.items():
|
|
1680
|
-
lines.append("")
|
|
1681
|
-
lines.append(title)
|
|
1682
|
-
lines.append("")
|
|
1683
|
-
for dest, _short, long_help in rows:
|
|
1684
|
-
option = "--" + dest.replace("_", "-")
|
|
1685
|
-
lines.append(f" {option}")
|
|
1686
|
-
for para in long_help.split("\n"):
|
|
1687
|
-
lines.extend(
|
|
1688
|
-
textwrap.wrap(
|
|
1689
|
-
para,
|
|
1690
|
-
width=88,
|
|
1691
|
-
initial_indent=" ",
|
|
1692
|
-
subsequent_indent=" ",
|
|
1693
|
-
)
|
|
1694
|
-
or [" "]
|
|
1695
|
-
)
|
|
1696
|
-
print("\n".join(lines))
|
|
1746
|
+
print(_repair_flag_reference())
|
|
1697
1747
|
parser.exit(0)
|
|
1698
1748
|
|
|
1699
1749
|
|
|
@@ -1866,6 +1866,54 @@ class TestHelpLayout(unittest.TestCase):
|
|
|
1866
1866
|
self.assertIn("XML NameChar set", text, verb)
|
|
1867
1867
|
self.assertIn("no-worse", text, verb)
|
|
1868
1868
|
|
|
1869
|
+
def test_help_repairs_theme_codes_when_colored(self):
|
|
1870
|
+
from bindery import cli
|
|
1871
|
+
|
|
1872
|
+
text = cli._repair_flag_reference(color=True)
|
|
1873
|
+
# the exact CPython 3.14 argparse theme: heading blue, prog magenta,
|
|
1874
|
+
# long-option cyan
|
|
1875
|
+
self.assertIn("\x1b[1;34m", text)
|
|
1876
|
+
self.assertIn("\x1b[35mbindery\x1b[0m", text)
|
|
1877
|
+
self.assertIn("\x1b[1;36m--fix-ids\x1b[0m", text)
|
|
1878
|
+
self.assertIn("\x1b[1;34mlossy strips", text)
|
|
1879
|
+
|
|
1880
|
+
def test_help_repairs_strip_invariant(self):
|
|
1881
|
+
# colored, codes stripped == plain, byte for byte: one layout path
|
|
1882
|
+
# serves both faces (the lattice-music invariant)
|
|
1883
|
+
import re
|
|
1884
|
+
|
|
1885
|
+
from bindery import cli
|
|
1886
|
+
|
|
1887
|
+
colored = cli._repair_flag_reference(color=True)
|
|
1888
|
+
plain = cli._repair_flag_reference(color=False)
|
|
1889
|
+
self.assertEqual(re.sub(r"\x1b\[[0-9;]*m", "", colored), plain)
|
|
1890
|
+
|
|
1891
|
+
def test_help_repairs_piped_output_is_plain(self):
|
|
1892
|
+
# a pipe (tests, README embedding) never carries codes
|
|
1893
|
+
for verb in ("repair", "library"):
|
|
1894
|
+
buf = io.StringIO()
|
|
1895
|
+
with self.assertRaises(SystemExit), contextlib.redirect_stdout(buf):
|
|
1896
|
+
main([verb, "--help-repairs"])
|
|
1897
|
+
self.assertNotIn("\x1b[", buf.getvalue(), verb)
|
|
1898
|
+
|
|
1899
|
+
def test_help_gate_tracks_argparse(self):
|
|
1900
|
+
# the gate IS argparse's own detection on 3.14+ (so -h and
|
|
1901
|
+
# --help-repairs can never disagree); plain False below, where
|
|
1902
|
+
# argparse has no colorization
|
|
1903
|
+
import os
|
|
1904
|
+
|
|
1905
|
+
from bindery import cli
|
|
1906
|
+
|
|
1907
|
+
if sys.version_info < (3, 14):
|
|
1908
|
+
self.assertFalse(cli._can_color_help())
|
|
1909
|
+
return
|
|
1910
|
+
from _colorize import can_colorize
|
|
1911
|
+
|
|
1912
|
+
self.assertEqual(cli._can_color_help(), can_colorize())
|
|
1913
|
+
# NO_COLOR suppresses on every interpreter
|
|
1914
|
+
with mock.patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
1915
|
+
self.assertFalse(cli._can_color_help())
|
|
1916
|
+
|
|
1869
1917
|
def test_help_repairs_is_not_a_selection_flag(self):
|
|
1870
1918
|
# _HelpRepairsAction is deliberately not a store_true: the wiring
|
|
1871
1919
|
# test counts store_true dests as repair selections
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|