Source code for betty.requirement

"""
Provide an API that lets code express arbitrary requirements.
"""

from __future__ import annotations

from textwrap import indent
from typing import cast, Any, Self

from betty.error import UserFacingError
from betty.locale import Str, Localizable, Localizer


[docs] class Requirement(Localizable):
[docs] def is_met(self) -> bool: raise NotImplementedError(repr(self))
[docs] def assert_met(self) -> None: if not self.is_met(): raise RequirementError(self) return None
[docs] def summary(self) -> Str: raise NotImplementedError(repr(self))
[docs] def details(self) -> Str | None: return None
[docs] def localize(self, localizer: Localizer) -> str: string = self.summary().localize(localizer) details = self.details() if details is not None: string += f'\n{"-" * len(string)}' string += f"\n{details.localize(localizer)}" return string
[docs] def reduce(self) -> Requirement | None: """ Remove unnecessary components of this requirement. - Collections can flatten unnecessary hierarchies. - Empty decorators or collections can 'dissolve' themselves and return None. This function MUST NOT modify self. """ return self
[docs] class RequirementError(UserFacingError, RuntimeError): def __init__(self, requirement: Requirement): super().__init__(requirement) self._requirement = requirement def __reduce__(self) -> tuple[type[Self], tuple[Requirement]]: return type(self), (self._requirement,)
[docs] def requirement(self) -> Requirement: return self._requirement
[docs] class RequirementCollection(Requirement): def __init__(self, *requirements: Requirement | None): super().__init__() self._requirements: list[Requirement] = [ requirement for requirement in requirements if requirement ] def __eq__(self, other: Any) -> bool: if not isinstance(other, type(self)): return False return self._requirements == other._requirements def __add__(self, other: Any) -> Self: if not isinstance(other, Requirement): raise NotImplementedError(repr(self)) self._requirements = [*self._requirements, other] return self
[docs] def localize(self, localizer: Localizer) -> str: localized = super().localize(localizer) for requirement in self._requirements: localized += f'\n-{indent(requirement.localize(localizer), " ")[1:]}' return localized
[docs] def reduce(self) -> Requirement | None: reduced_requirements = [] for requirement in self._requirements: reduced_requirement = requirement.reduce() if reduced_requirement: if type(reduced_requirement) is type(self): reduced_requirements.extend( cast(RequirementCollection, reduced_requirement)._requirements ) else: reduced_requirements.append(reduced_requirement) if len(reduced_requirements) == 1: return reduced_requirements[0] if reduced_requirements: return type(self)(*reduced_requirements) return None
[docs] class AnyRequirement(RequirementCollection): def __init__(self, *requirements: Requirement | None): super().__init__(*requirements) self._summary = Str._("One or more of these requirements must be met")
[docs] def is_met(self) -> bool: for requirement in self._requirements: if requirement.is_met(): return True return False
[docs] def summary(self) -> Str: return self._summary
[docs] class AllRequirements(RequirementCollection): def __init__(self, *requirements: Requirement | None): super().__init__(*requirements) self._summary = Str._("All of these requirements must be met")
[docs] def is_met(self) -> bool: for requirement in self._requirements: if not requirement.is_met(): return False return True
[docs] def summary(self) -> Str: return self._summary