Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/scribae/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Any, Literal, cast

import frontmatter
import yaml
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
from pydantic_ai import Agent, NativeOutput, UnexpectedModelBehavior
from pydantic_ai.settings import ModelSettings
Expand Down Expand Up @@ -603,7 +604,7 @@ def _load_body(body_path: Path, *, max_chars: int) -> BodyDocument:
raise FeedbackFileError(f"Draft file not found: {body_path}") from exc
except OSError as exc: # pragma: no cover - surfaced by CLI
raise FeedbackFileError(f"Unable to read draft: {exc}") from exc
except Exception as exc: # pragma: no cover - parsing errors
except (yaml.YAMLError, TypeError, ValueError) as exc: # pragma: no cover - parsing errors
raise FeedbackFileError(f"Unable to parse draft {body_path}: {exc}") from exc

metadata = dict(post.metadata or {})
Expand Down
3 changes: 2 additions & 1 deletion src/scribae/io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

import frontmatter
import yaml

# Type alias for verbose output callbacks used across modules.
Reporter = Callable[[str], None] | None
Expand Down Expand Up @@ -38,7 +39,7 @@ def load_note(note_path: Path, *, max_chars: int) -> NoteDetails:
raise FileNotFoundError(f"Note file not found: {note_path}") from exc
except OSError as exc: # pragma: no cover - surfaced to CLI
raise OSError(f"Unable to read note {note_path}: {exc}") from exc
except Exception as exc: # pragma: no cover - parsing errors
except (yaml.YAMLError, TypeError, ValueError) as exc: # pragma: no cover - parsing errors
raise ValueError(f"Unable to parse note {note_path}: {exc}") from exc

metadata = dict(post.metadata or {})
Expand Down
4 changes: 2 additions & 2 deletions src/scribae/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def _detect_language(text: str, language_detector: Callable[[str], str] | None)
return normalize_language(detector(sample))
except LanguageResolutionError:
raise
except Exception as exc:
except (AttributeError, TypeError, ValueError, RuntimeError) as exc:
raise LanguageResolutionError(f"Language detection failed: {exc}") from exc


Expand All @@ -143,7 +143,7 @@ def _default_language_detector() -> Callable[[str], str]:

try:
detector = LanguageDetectorBuilder.from_all_languages().build()
except Exception as exc: # pragma: no cover - defensive fallback
except (AttributeError, TypeError, ValueError, RuntimeError) as exc: # pragma: no cover - defensive fallback
return _naive_detector(exc)

def _detect(sample: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/scribae/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def _load_body(body_path: Path, *, max_chars: int) -> BodyDocument:
raise MetaFileError(f"Body file not found: {body_path}") from exc
except OSError as exc: # pragma: no cover - surfaced by CLI
raise MetaFileError(f"Unable to read body: {exc}") from exc
except Exception as exc: # pragma: no cover - parsing errors
except (yaml.YAMLError, TypeError, ValueError) as exc: # pragma: no cover - parsing errors
raise MetaFileError(f"Unable to parse body {body_path}: {exc}") from exc

metadata = dict(post.metadata or {})
Expand Down
6 changes: 3 additions & 3 deletions src/scribae/translate_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
TranslationConfig,
TranslationPipeline,
)
from scribae.translate.postedit import PostEditAborted

translate_app = typer.Typer()

Expand Down Expand Up @@ -60,7 +61,6 @@ def _configure_library_logging() -> None:
pass



def _load_glossary(path: Path | None) -> dict[str, str]:
if path is None:
return {}
Expand Down Expand Up @@ -284,7 +284,7 @@ def translate(
if reporter:
reporter("Fetching post-edit language model...")
posteditor.prefetch_language_model()
except Exception as exc:
except (PostEditAborted, RuntimeError) as exc:
if not prefetch_only and "nllb" in cfg.mt_backend:
secho_info(
"Primary MT model prefetch failed; falling back to NLLB.",
Expand All @@ -295,7 +295,7 @@ def translate(
steps = registry.route(resolved_src, tgt, allow_pivot=False, backend=cfg.mt_backend)
try:
mt.prefetch(steps)
except Exception as fallback_exc:
except RuntimeError as fallback_exc:
typer.secho(str(fallback_exc), err=True, fg=typer.colors.RED)
raise typer.Exit(4) from fallback_exc
else:
Expand Down
36 changes: 35 additions & 1 deletion tests/unit/io_utils_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from __future__ import annotations

from scribae.io_utils import Reporter, truncate
from pathlib import Path

import pytest
import yaml

from scribae.io_utils import Reporter, load_note, truncate


class TestTruncate:
Expand Down Expand Up @@ -76,3 +81,32 @@ def dummy(msg: str) -> None:
r2: Reporter = None
assert callable(r1)
assert r2 is None


class TestLoadNoteErrors:
def test_load_note_wraps_value_error_as_parse_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
def _boom(_path: Path) -> None:
raise ValueError("invalid frontmatter")

monkeypatch.setattr("scribae.io_utils.frontmatter.load", _boom)

with pytest.raises(ValueError, match="Unable to parse note"):
load_note(Path("note.md"), max_chars=100)

def test_load_note_wraps_yaml_error_as_parse_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
def _boom(_path: Path) -> None:
raise yaml.YAMLError("bad yaml")

monkeypatch.setattr("scribae.io_utils.frontmatter.load", _boom)

with pytest.raises(ValueError, match="Unable to parse note"):
load_note(Path("note.md"), max_chars=100)

def test_load_note_does_not_mask_unexpected_runtime_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
def _boom(_path: Path) -> None:
raise RuntimeError("unexpected parser failure")

monkeypatch.setattr("scribae.io_utils.frontmatter.load", _boom)

with pytest.raises(RuntimeError, match="unexpected parser failure"):
load_note(Path("note.md"), max_chars=100)
24 changes: 24 additions & 0 deletions tests/unit/translate_cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,30 @@ def _raise(self: object, steps: list[object]) -> None:
assert "prefetch failed" in ansi_stripped


def test_translate_prefetch_does_not_swallow_unexpected_errors(
monkeypatch: pytest.MonkeyPatch,
stub_translation_components: dict[str, Any],
) -> None:
def _raise(self: object, steps: list[object]) -> None:
raise ValueError("unexpected")

monkeypatch.setattr("scribae.translate_cli.MTTranslator.prefetch", _raise)

result = runner.invoke(
app,
[
"translate",
"--src",
"en",
"--tgt",
"de",
"--prefetch-only",
],
)

assert isinstance(result.exception, ValueError)


def test_translate_warns_on_source_mismatch(
monkeypatch: pytest.MonkeyPatch,
stub_translation_components: dict[str, Any],
Expand Down