docx-redline v1.0.2
26

The exception hierarchy, and what raises what

RedlineError base: an edit could not be located or applied ClauseError a clause number could not be resolved StalePlanError a plan was computed against a different version RedlineCredentialsError a model-backed reviewer has no API key

examples/26_errors.py โ€” ran in 0.35s ยท exit 0

Console output

============================================================================
26 ยท Errors
============================================================================

--- the hierarchy ---
  RedlineError             RuntimeError <- Exception <- BaseException
  ClauseError              RedlineError <- RuntimeError <- Exception
  StalePlanError           RedlineError <- RuntimeError <- Exception
  RedlineCredentialsError  RuntimeError <- Exception <- BaseException
  catching RedlineError catches all of them

--- RedlineError โ€” locating failures ---

  find_paragraph matched nothing
    RedlineError: no paragraph matched {'contains': 'not in this contract'}

  an unknown paragraph style
    RedlineError: unknown paragraph style 'Nonesuch'; this document defines 'Heading 1', 'Heading 2', 'Heading 3',

  a strict op that matched nothing
    RedlineError: [0] op 'replace_text' matched nothing: {'op': 'replace_text', 'old': 'absent', 'new': 'x'}

  ParagraphIndex.paragraph out of range
    RedlineError: para_id 9999 out of range (0-91)

  a malformed model payload
    RedlineError: malformed edit payload:
      #0: 'edit' does not accept 'replacment'

  an action plan that fails its schema check
    RedlineError: invalid action items:
      X: 'renumber_clause' is derived by the planner and must not be supplied

  full_redline with nothing to do
    RedlineError: full_redline needs something to do: pass revised=, actions=, reviewer= or comments=

--- ClauseError โ€” a clause number that does not resolve ---

  a hallucinated clause number
    ClauseError: no clause numbered '99.9' in the document

  an action pointing at one
    ClauseError: no clause numbered '99.9' in the document
    a hallucinated clause fails loudly at the plan stage, instead of
    silently editing the wrong paragraph

--- StalePlanError โ€” the plan and the document disagree ---

  re-verifying after the document moved on
    StalePlanError: document moved on: plan was computed against 902892107d00df8a, document is now 95cb5abfbfa477f2.

--- RedlineCredentialsError โ€” names the variable to set ---

  OpenAIReviewer
    RuntimeError: the OpenAI reviewer needs the openai SDK: pip install openai

  ClaudeReviewer
    RuntimeError: the Claude reviewer needs the anthropic SDK: pip install anthropic

    the two SDKs fail at different moments -- OpenAI refuses to build a
    client without a key, Anthropic builds one and rejects the first
    request -- so the translation wraps both

--- recording a failure instead of raising ---
  AI-001 applied  replaced 'thirty (30) days' -> 'forty-five (45) days'
  AI-002 failed   text 'not in this clause' not found in scope
  strict=False records; strict=True raises on the first failure

--- and Rejection, which is data rather than an exception ---
  <Rejection.TARGET_AMBIGUOUS: 'target_ambiguous'>
  span occurs more than once and is under 25 characters; quote more context, set occurrence=n, or set occurrence=0 for all of them
  the ParagraphIndex layer reports rather than raises, so one bad edit in
  a batch of forty does not lose the other thirty-nine

Source

"""26 ยท The exception hierarchy, and what raises what.

RedlineError              base: an edit could not be located or applied
  ClauseError             a clause number could not be resolved
  StalePlanError          a plan was computed against a different version
  RedlineCredentialsError a model-backed reviewer has no API key
"""

import docx
from _shared import OUT, SOURCE, banner, fresh, section

from docx_redline import (
    ClauseError,
    ClauseTree,
    ParagraphIndex,
    RedlineCredentialsError,
    RedlineEdit,
    RedlineError,
    StalePlanError,
    apply_actions,
    full_redline,
    load_edits,
    verify_plan,
)
from docx_redline.editing.ops import apply_operations

banner("26 ยท Errors")

section("the hierarchy")
for exc in (RedlineError, ClauseError, StalePlanError, RedlineCredentialsError):
    bases = " <- ".join(c.__name__ for c in exc.__mro__[1:4])
    print(f"  {exc.__name__:<24} {bases}")
print("  catching RedlineError catches all of them")


def raises(title, fn):
    try:
        fn()
    except Exception as exc:
        print(f"\n  {title}")
        lines = [line for line in str(exc).splitlines() if line.strip()]
        print(f"    {type(exc).__name__}: {lines[0][:96]}")
        for extra in lines[1:2]:
            print(f"      {extra.strip()[:94]}")


section("RedlineError โ€” locating failures")
raises(
    "find_paragraph matched nothing",
    lambda: fresh().find_paragraph(contains="not in this contract"),
)
raises(
    "an unknown paragraph style",
    lambda: fresh().apply_style(fresh().find_paragraph(contains="3.2  Invoicing"), "Nonesuch"),
)
raises(
    "a strict op that matched nothing",
    lambda: apply_operations(
        fresh(), [{"op": "replace_text", "old": "absent", "new": "x"}], strict=True
    ),
)
raises("ParagraphIndex.paragraph out of range", lambda: ParagraphIndex(fresh()).paragraph(9999))
raises(
    "a malformed model payload",
    lambda: load_edits([{"para_id": 0, "target": "x", "replacment": "y"}]),
)
raises(
    "an action plan that fails its schema check",
    lambda: apply_actions(fresh(), [{"id": "X", "type": "renumber_clause", "clause": "3.1"}]),
)
# the output path is never written -- the call raises before it gets that far
raises("full_redline with nothing to do", lambda: full_redline(SOURCE, OUT / "never.docx"))

section("ClauseError โ€” a clause number that does not resolve")
tree = ClauseTree(docx.Document(SOURCE).element.body)
raises("a hallucinated clause number", lambda: tree.get("99.9"))
raises(
    "an action pointing at one",
    lambda: apply_actions(
        fresh(), [{"id": "X", "type": "delete_clause", "clause": "99.9"}], strict=True
    ),
)
print("    a hallucinated clause fails loudly at the plan stage, instead of")
print("    silently editing the wrong paragraph")

section("StalePlanError โ€” the plan and the document disagree")
rl = fresh()
index = ParagraphIndex(rl)
stale = index.fingerprint()
index.apply([RedlineEdit(19, "thirty (30) days", "forty-five (45) days")])
raises("re-verifying after the document moved on", lambda: verify_plan(index, stale))

section("RedlineCredentialsError โ€” names the variable to set")
import os

saved = {k: os.environ.pop(k, None) for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY")}
try:
    from docx_redline import ClaudeReviewer, OpenAIReviewer

    for cls in (OpenAIReviewer, ClaudeReviewer):
        raises(cls.__name__, lambda cls=cls: cls().propose(tree, "Review this."))
finally:
    for k, v in saved.items():
        if v is not None:
            os.environ[k] = v
print("\n    the two SDKs fail at different moments -- OpenAI refuses to build a")
print("    client without a key, Anthropic builds one and rejects the first")
print("    request -- so the translation wraps both")

section("recording a failure instead of raising")
report = apply_actions(
    fresh(),
    [
        {
            "id": "AI-001",
            "type": "replace_text",
            "clause": "3.2",
            "find": "thirty (30) days",
            "replace": "forty-five (45) days",
        },
        {
            "id": "AI-002",
            "type": "replace_text",
            "clause": "3.2",
            "find": "not in this clause",
            "replace": "x",
        },
    ],
    strict=False,
)
for res in report.results:
    print(f"  {res.id} {res.status:<8} {res.detail[:64]}")
print("  strict=False records; strict=True raises on the first failure")

section("and Rejection, which is data rather than an exception")
report = ParagraphIndex(fresh()).apply([RedlineEdit(6, "Provider", "Vendor")])
res = report.rejected[0]
print(f"  {res.reason!r}")
print(f"  {res.detail}")
print("  the ParagraphIndex layer reports rather than raises, so one bad edit in")
print("  a batch of forty does not lose the other thirty-nine")

What it wrote

This example prints its result rather than saving a document โ€” read the console output beside it.