"""
Provide assertion failures.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from contextlib import contextmanager
from textwrap import indent
from typing import Iterator, Self, TYPE_CHECKING, TypeAlias, TypeVar
from typing_extensions import override
from betty.error import UserFacingError
from betty.locale import UNDETERMINED_LOCALE
from betty.locale.localizable import _, Localizable
from betty.locale.localized import LocalizedStr
if TYPE_CHECKING:
from collections.abc import Sequence, MutableSequence
from betty.locale.localizer import Localizer
_AssertionContextValueT = TypeVar("_AssertionContextValueT")
[docs]
class AssertionContext(ABC):
"""
The context in which an assertion is invoked.
"""
[docs]
@abstractmethod
def format(self) -> str:
"""
Format this context to a string.
"""
pass
[docs]
class Attr(AssertionContext):
"""
An object attribute context.
"""
[docs]
def raised(self, error_type: type[AssertionFailed]) -> bool:
"""
Check if the error matches the given error type.
"""
return isinstance(self, error_type)
@property
def contexts(self) -> tuple[Contextey, ...]:
"""
Get the human-readable contexts describing where the error occurred in the source data.
"""
return self._contexts
[docs]
@override
def raised(self, error_type: type[AssertionFailed]) -> bool:
return any(error.raised(error_type) for error in self._errors)
@property
def valid(self) -> bool:
"""
Check that this collection contains no errors.
"""
return len(self._errors) == 0
@property
def invalid(self) -> bool:
"""
Check that this collection contains at least one error.
"""
return not self.valid
[docs]
@contextmanager
def assert_valid(self, *contexts: Contextey) -> Iterator[Self]:
"""
Assert that this collection contains no errors.
"""
if self.invalid:
raise self
with self.catch(*contexts):
yield self
if self.invalid: # type: ignore[redundant-expr]
raise self
[docs]
def append(self, *errors: AssertionFailed) -> None:
"""
Append errors to this collection.
"""
for error in errors:
if isinstance(error, AssertionFailedGroup):
self.append(*error)
else:
self._errors.append(error.with_context(*self._contexts))
[docs]
@override
def with_context(self, *contexts: Contextey) -> Self:
self_copy = super().with_context(*contexts)
self_copy._errors = [error.with_context(*contexts) for error in self._errors]
return self_copy
[docs]
@contextmanager
def catch(self, *contexts: Contextey) -> Iterator[AssertionFailedGroup]:
"""
Catch any errors raised within this context manager and add them to the collection.
:return: A new collection that will only contain any newly raised errors.
"""
context_errors: AssertionFailedGroup = AssertionFailedGroup()
if contexts:
context_errors = context_errors.with_context(*contexts)
try:
yield context_errors
except AssertionFailed as e:
context_errors.append(e)
self.append(*context_errors)