from __future__ import annotations
from pathlib import Path
import pytest
import mrrc
import mrrc.exceptions as mexc
_REPO_ROOT = Path(__file__).resolve().parents[2]
PYMARC_CLASS_NAMES_MRRC_PROVIDES = [
"RecordLengthInvalid",
"RecordLeaderInvalid",
"RecordDirectoryInvalid",
"BaseAddressInvalid",
"BaseAddressNotFound",
"EndOfRecordNotFound",
"TruncatedRecord",
"FieldNotFound",
"FatalReaderError",
"BadSubfieldCodeWarning",
]
PYMARC_CLASS_NAMES_MRRC_OMITS = {
"NoFieldsFound": (
"mrrc does not raise on records with zero fields; an empty "
"Record is a valid in-memory state."
),
"WriteNeedsRecord": (
"mrrc.MARCWriter is type-annotated; passing a non-Record is "
"caught by static type checking, not a runtime exception."
),
"NoActiveFile": (
"mrrc.MARCWriter manages file lifecycle via context manager; "
"operating on a closed writer raises RuntimeError, not a "
"typed mrrc exception."
),
"BadLeaderValue": (
"mrrc.Leader validates fields at construction via its typed "
"field accessors; bad values surface as ValueError, not as "
"a typed mrrc exception."
),
"MissingLinkedFields": (
"880-linkage validation isn't implemented in mrrc; linked-"
"field consistency is out of scope for the parser."
),
}
@pytest.mark.parametrize("name", PYMARC_CLASS_NAMES_MRRC_PROVIDES)
def test_pymarc_compatible_name_is_importable_from_exceptions(name: str) -> None:
assert hasattr(mexc, name), (
f"mrrc.exceptions is missing {name} — pymarc-shape error-handling "
f"code that catches {name} by name would fail at import."
)
@pytest.mark.parametrize("name", PYMARC_CLASS_NAMES_MRRC_PROVIDES)
def test_pymarc_compatible_class_is_catchable(name: str) -> None:
cls = getattr(mexc, name)
assert issubclass(cls, (Exception, Warning)), (
f"{name} ({cls!r}) is not catchable as Exception/Warning"
)
@pytest.mark.parametrize("name", PYMARC_CLASS_NAMES_MRRC_PROVIDES)
def test_pymarc_compatible_class_is_mrrc_branded(name: str) -> None:
if name == "BadSubfieldCodeWarning":
pytest.skip("BadSubfieldCodeWarning is a Warning, not an Exception")
cls = getattr(mexc, name)
assert issubclass(cls, mexc.MrrcException), (
f"{name} is not a subclass of MrrcException — pymarc users "
f"porting code with `except MrrcException:` won't catch it."
)
@pytest.mark.parametrize("name", PYMARC_CLASS_NAMES_MRRC_PROVIDES)
def test_pymarc_compatible_name_re_exported_from_top_level_mrrc(name: str) -> None:
if name == "BadSubfieldCodeWarning":
if not hasattr(mrrc, name):
pytest.skip(
f"{name} not re-exported on top-level mrrc; documented "
f"as accessible via mrrc.exceptions"
)
assert hasattr(mrrc, name), (
f"mrrc.{name} not re-exported; pymarc users must change "
f"`from pymarc import {name}` to "
f"`from mrrc.exceptions import {name}` instead of "
f"`from mrrc import {name}`."
)
@pytest.mark.parametrize("name,reason", PYMARC_CLASS_NAMES_MRRC_OMITS.items())
def test_omitted_pymarc_name_is_documented(name: str, reason: str) -> None:
assert not hasattr(mexc, name), (
f"mrrc.exceptions.{name} now exists but is documented as "
f"deliberately omitted ({reason}). Update "
f"PYMARC_CLASS_NAMES_MRRC_OMITS and the migration doc."
)
assert reason, f"Omission of {name} must carry a documented reason"
def test_specific_class_except_catches_mrrc_raised_exception() -> None:
bad_bytes = (
_REPO_ROOT / "tests" / "data" / "error_fixtures" / "e101_non_ascii_tag.bin"
).read_bytes()
reader = mrrc.MARCReader(
bad_bytes, recovery_mode="strict", validation_level="strict_marc"
)
try:
next(reader)
except mexc.RecordDirectoryInvalid:
pass
except Exception as e:
pytest.fail(
f"expected RecordDirectoryInvalid to be raised and caught; "
f"got {type(e).__name__}: {e}"
)
else:
pytest.fail("expected RecordDirectoryInvalid; nothing raised")
def test_mrrc_exception_base_catches_every_typed_variant() -> None:
bad_bytes = b"X0150nam a2200061 4500" reader = mrrc.MARCReader(bad_bytes)
try:
next(reader)
except mexc.MrrcException:
pass
except Exception as e:
pytest.fail(
f"expected MrrcException to catch the parse failure; "
f"got bare {type(e).__name__}: {e}"
)
else:
pytest.fail("expected an exception; nothing raised")