Skip to content

Commit 0ec3435

Browse files
committed
test: improved verbosity in error-checking
1 parent 0ed967c commit 0ec3435

17 files changed

Lines changed: 590 additions & 528 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ install:
1515
uv venv && uv pip install -e '.[dev]'
1616

1717
lint:
18-
uv run ruff check . && uv run mypy .
18+
uv run ruff check . && uv run mypy src
1919

2020
test:
2121
PYTHONPATH=src uv run pytest

src/trustforge/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from __future__ import annotations
2+
3+
__all__ = ["__version__"]
4+
__version__ = "0.1.0"

src/trustforge/cli.py

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,74 @@
11
from __future__ import annotations
22

3+
import sys
34
from pathlib import Path
45

56
import typer
67

7-
from .errors import (
8-
AssetMissingError,
9-
DependencyMissingError,
10-
HTMLBuildError,
11-
InvalidFrontmatterError,
12-
PDFBuildError,
13-
ThemeLoadError,
14-
TrustforgeError,
15-
)
8+
from .common.errors import TrustforgeError
169
from .pipeline.metadata import build_index_csv
1710
from .pipeline.render_html import render_html
1811
from .pipeline.render_pdf import render_pdf
1912

2013
app = typer.Typer(help="Trustforge Policy Foundry")
2114

2215

23-
def _bail(msg: str, code: int = 1) -> None:
24-
typer.secho(msg, fg=typer.colors.RED, err=True)
25-
raise typer.Exit(code)
16+
def _echo_err(msg: str) -> None:
17+
typer.echo(msg, err=True)
2618

2719

2820
@app.command()
2921
def html(policy: str) -> None:
30-
"""Render a policy Markdown file to themed HTML."""
22+
"""
23+
Render a policy Markdown file to themed HTML.
24+
"""
3125
try:
3226
out = render_html(Path(policy))
33-
typer.secho(f"HTML -> {out}", fg=typer.colors.GREEN)
34-
except InvalidFrontmatterError as e:
35-
_bail(f"[frontmatter] {e}")
36-
except (AssetMissingError, ThemeLoadError) as e:
37-
_bail(f"[theme/assets] {e}")
38-
except HTMLBuildError as e:
39-
_bail(f"[html] {e}")
4027
except TrustforgeError as e:
41-
_bail(f"[trustforge] {e}")
28+
_echo_err(str(e))
29+
raise typer.Exit(code=2)
4230
except Exception as e: # safety net
43-
_bail(f"[unexpected] {e.__class__.__name__}: {e}")
31+
_echo_err(f"Unexpected error: {e}")
32+
raise typer.Exit(code=1)
33+
typer.echo(f"HTML -> {out}")
4434

4535

4636
@app.command()
4737
def pdf(policy: str) -> None:
48-
"""Render a policy Markdown file to themed PDF (xelatex required)."""
38+
"""
39+
Render a policy Markdown file to themed PDF (XeLaTeX required).
40+
"""
4941
try:
5042
out = render_pdf(Path(policy))
51-
typer.secho(f"PDF -> {out}", fg=typer.colors.GREEN)
52-
except InvalidFrontmatterError as e:
53-
_bail(f"[frontmatter] {e}")
54-
except DependencyMissingError as e:
55-
_bail(f"[deps] {e}\nHint: install TeX Live (XeLaTeX).")
56-
except AssetMissingError as e:
57-
_bail(f"[assets] {e}")
58-
except ThemeLoadError as e:
59-
_bail(f"[theme] {e}")
60-
except PDFBuildError as e:
61-
_bail(f"[pdf] {e}")
6243
except TrustforgeError as e:
63-
_bail(f"[trustforge] {e}")
64-
except Exception as e: # safety net
65-
_bail(f"[unexpected] {e.__class__.__name__}: {e}")
44+
_echo_err(str(e))
45+
raise typer.Exit(code=2)
46+
except Exception as e:
47+
_echo_err(f"Unexpected error: {e}")
48+
raise typer.Exit(code=1)
49+
typer.echo(f"PDF -> {out}")
6650

6751

6852
@app.command()
6953
def index(out: str = "out/policies.csv", policies_dir: str = "policies") -> None:
70-
"""Build CSV index of policy frontmatter across all markdown files."""
54+
"""
55+
Build CSV index of policy frontmatter across all markdown files.
56+
"""
7157
try:
7258
p = build_index_csv(Path(policies_dir), Path(out))
73-
typer.secho(f"Index -> {p}", fg=typer.colors.GREEN)
74-
except InvalidFrontmatterError as e:
75-
_bail(f"[frontmatter] {e}")
7659
except TrustforgeError as e:
77-
_bail(f"[trustforge] {e}")
78-
except FileNotFoundError:
79-
_bail(f"[inputs] Policies dir not found: {policies_dir}")
80-
except Exception as e: # safety net
81-
_bail(f"[unexpected] {e.__class__.__name__}: {e}")
60+
_echo_err(str(e))
61+
raise typer.Exit(code=2)
62+
except Exception as e:
63+
_echo_err(f"Unexpected error: {e}")
64+
raise typer.Exit(code=1)
65+
typer.echo(f"Index -> {p}")
8266

8367

8468
if __name__ == "__main__":
85-
app()
69+
# Typer uses SystemExit; this guard ensures clean non-zero codes propagate.
70+
try:
71+
app()
72+
except SystemExit as e:
73+
# Allow GH Actions to display a non-zero exit when appropriate.
74+
sys.exit(e.code)

src/trustforge/common/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
# Re-export errors so "from trustforge.common import FrontmatterError" also works.
4+
from .errors import (
5+
AssetNotFoundError,
6+
CSVIndexError,
7+
FrontmatterError,
8+
LaTeXError,
9+
MissingDependencyError,
10+
RenderError,
11+
TemplateError,
12+
TrustforgeError,
13+
)
14+
15+
__all__ = [
16+
"TrustforgeError",
17+
"FrontmatterError",
18+
"TemplateError",
19+
"AssetNotFoundError",
20+
"MissingDependencyError",
21+
"LaTeXError",
22+
"CSVIndexError",
23+
"RenderError",
24+
]

src/trustforge/common/errors.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
6+
class TrustforgeError(Exception):
7+
"""Base class for all Trustforge errors."""
8+
9+
10+
class FrontmatterError(TrustforgeError):
11+
"""Raised when a policy markdown file has missing/invalid YAML frontmatter."""
12+
13+
def __init__(self, source: str | Path, message: str) -> None:
14+
super().__init__(f"{source}: {message}")
15+
self.source = str(source)
16+
17+
18+
class TemplateError(TrustforgeError):
19+
"""Raised when a template (HTML/LaTeX) is missing or malformed."""
20+
21+
def __init__(self, template: str | Path, message: str) -> None:
22+
super().__init__(f"Template error [{template}]: {message}")
23+
self.template = str(template)
24+
25+
26+
class AssetNotFoundError(TrustforgeError):
27+
"""Raised when a referenced asset (logo, font, etc.) cannot be found."""
28+
29+
def __init__(self, asset: str | Path) -> None:
30+
super().__init__(f"Asset not found: {asset}")
31+
self.asset = str(asset)
32+
33+
34+
class MissingDependencyError(TrustforgeError):
35+
"""Raised when a required system dependency is missing (e.g., xelatex)."""
36+
37+
def __init__(self, binary: str) -> None:
38+
super().__init__(f"Missing required dependency: {binary}. Please install it and retry.")
39+
self.binary = binary
40+
41+
42+
class LaTeXError(TrustforgeError):
43+
"""Raised when XeLaTeX returns a non-zero exit status."""
44+
45+
def __init__(self, tex_file: str | Path, log_file: str | Path | None = None) -> None:
46+
msg = f"LaTeX failed compiling: {tex_file}"
47+
if log_file:
48+
msg += f" (see log: {log_file})"
49+
super().__init__(msg)
50+
self.tex_file = str(tex_file)
51+
self.log_file = str(log_file) if log_file else None
52+
53+
54+
class CSVIndexError(TrustforgeError):
55+
"""Raised for CSV index build problems (e.g., missing policies dir)."""
56+
57+
def __init__(self, location: str | Path, message: str) -> None:
58+
super().__init__(f"CSV index error at [{location}]: {message}")
59+
self.location = str(location)
60+
61+
62+
class RenderError(TrustforgeError):
63+
"""Generic render pipeline error wrapper."""
64+
65+
def __init__(self, stage: str, message: str) -> None:
66+
super().__init__(f"Render error during '{stage}': {message}")
67+
self.stage = stage
68+
69+
70+
__all__ = [
71+
"TrustforgeError",
72+
"FrontmatterError",
73+
"TemplateError",
74+
"AssetNotFoundError",
75+
"MissingDependencyError",
76+
"LaTeXError",
77+
"CSVIndexError",
78+
"RenderError",
79+
]

src/trustforge/common/md.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

33
import re
4+
from pathlib import Path
45
from typing import cast
56

67
import yaml
78
from markdown_it import MarkdownIt
89

9-
from ..models import PolicyMeta
10+
from trustforge.common.errors import FrontmatterError
11+
from trustforge.models import PolicyMeta
1012

1113
# Robust frontmatter:
1214
# - optional UTF-8 BOM
@@ -18,12 +20,17 @@
1820
)
1921

2022

21-
def parse_policy_markdown(text: str) -> tuple[PolicyMeta, str]:
23+
def parse_policy_markdown(text: str, source: str | Path = "<memory>") -> tuple[PolicyMeta, str]:
24+
"""
25+
Parse a markdown string with YAML frontmatter and return (meta, body).
26+
Raises FrontmatterError if the frontmatter block is missing/invalid.
27+
"""
2228
m = FRONTMATTER_RE.match(text)
2329
if not m:
24-
raise ValueError(
30+
raise FrontmatterError(
31+
source,
2532
"Missing or invalid frontmatter (--- ... ---) at top of file. "
26-
"Ensure the file begins with a YAML block delimited by '---' lines."
33+
"Ensure the file begins with a YAML block delimited by '---' lines.",
2734
)
2835
meta_raw, body = m.group(1), m.group(2)
2936
meta = PolicyMeta(**(yaml.safe_load(meta_raw) or {}))
@@ -32,4 +39,5 @@ def parse_policy_markdown(text: str) -> tuple[PolicyMeta, str]:
3239

3340
def md_to_html(body_md: str) -> str:
3441
md = MarkdownIt("commonmark")
42+
# markdown-it types return Any; cast it to keep mypy happy.
3543
return cast(str, md.render(body_md))

0 commit comments

Comments
 (0)