Files
Vijay Janapa Reddi 27d4f8114b binder: fix repo-root, build, doctor and info bugs; clearer errors; dedup
Bug fixes
- Build lock is keyed on books/ so builds started from different
  directories block each other (fixes two tests broken by 89b828efa3).
- ConfigManager resolves the repo root when binder runs from books/ or
  binder/, so bib, newsletter, doctor and render paths are right.
- `build <word> --all` treats intro/scaling/agentic/physical as chapter
  names (it built every volume), and Ctrl-C stops a --all loop.
- doctor checked chapter names that no longer exist and failed on the
  generated _quarto.yml; info defaulted to a deleted books/contents.
- `headings apply --path <typo>` no longer rewrites the whole book;
  `bib clean` stops on unreadable chapters instead of dropping entries.
- Discovery falls back to a line scan on invalid YAML instead of crashing.
- Volume III/IV were labeled Volume II in layout and release output.

Output
- Unexpected errors print the command, exception type, and raising
  file:line (new cli/core/errors.py), with the traceback on stderr.
- Chapter-not-found suggests the closest names and the list command.
- Silent except blocks in bib lookups, info, headings, layout,
  maintenance, preview and debug now say what went wrong.

Cleanup
- Shared qmd_files_under/display_path, file-rewrite loop, doctor result
  helper, preview server, clean helpers, volume display names.
- Removed legacy import fallbacks, dead config attributes, legacy backup
  restore logic, and unused imports.
2026-10-19 19:28:49 +02:00

43 lines
1.5 KiB
Python

"""Turn unexpected exceptions into messages that say what failed and where.
Binder is driven by agents and CI as much as by people, so an unexpected error
must name the exception type, its message, and the source line that raised it,
not just ``str(exc)`` (which is empty for many exceptions).
"""
from __future__ import annotations
import sys
import traceback
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[3]
def _short_path(filename: str) -> str:
"""Return *filename* relative to the repository root when it lies inside it."""
try:
return Path(filename).resolve().relative_to(_REPO_ROOT).as_posix()
except ValueError:
return filename
def describe_exception(exc: BaseException) -> str:
"""Return ``"Type: message (at path:line in function)"`` for *exc*.
The location is the innermost traceback frame, which is where the error was
raised. It is omitted when the exception carries no traceback.
"""
message = str(exc).strip()
text = f"{type(exc).__name__}: {message}" if message else type(exc).__name__
frames = traceback.extract_tb(exc.__traceback__) if exc.__traceback__ else []
if frames:
frame = frames[-1]
text += f" (at {_short_path(frame.filename)}:{frame.lineno} in {frame.name})"
return text
def print_traceback(exc: BaseException) -> None:
"""Write the full traceback of *exc* to stderr, keeping stdout clean for ``--json``."""
traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr)