Source code for betty.requirement

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

from __future__ import annotations

from abc import abstractmethod
from textwrap import indent
from typing import cast, Any, TYPE_CHECKING, final

from typing_extensions import override

from betty.error import UserFacingError
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


[docs] class Requirement(Localizable): """ Express a requirement. """
[docs] @abstractmethod def is_met(self) -> bool: """ Check if the requirement is met. """ pass
[docs] def assert_met(self) -> None: """ Assert that the requirement is met. """ if not self.is_met(): raise RequirementError(self) return None
[docs] @abstractmethod def summary(self) -> Localizable: """ Get the requirement's human-readable summary. """ pass
[docs] def details(self) -> Localizable | None: """ Get the requirement's human-readable additional details. """ return None
[docs] @override def localize(self, localizer: Localizer) -> LocalizedStr: super_localized = self.summary().localize(localizer) details = self.details() localized: str = super_localized if details is not None: localized += f"\n{'-' * len(localized)}" localized += f"\n{details.localize(localizer)}" return LocalizedStr(localized, locale=super_localized.locale)
[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] @final class RequirementError(UserFacingError, RuntimeError): """ Raised when a requirement is not met. """
[docs] def __init__(self, requirement: Requirement): super().__init__(requirement) self._requirement = requirement
[docs] def requirement(self) -> Requirement: """ Get the requirement this error is for. """ return self._requirement
[docs] class RequirementCollection(Requirement): """ Provide a collection of zero or more requirements. """
[docs] def __init__(self, *requirements: Requirement | None): super().__init__() self._requirements: Sequence[Requirement] = [ requirement for requirement in requirements if requirement ]
@override def __eq__(self, other: Any) -> bool: if not isinstance(other, type(self)): return False return self._requirements == other._requirements
[docs] @override def localize(self, localizer: Localizer) -> LocalizedStr: super_localized = super().localize(localizer) localized: str = super_localized for requirement in self._requirements: localized += f"\n-{indent(requirement.localize(localizer), ' ')[1:]}" return LocalizedStr(localized, locale=super_localized.locale)
[docs] @override def reduce(self) -> Requirement | None: reduced_requirements: MutableSequence[Requirement] = [] 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] @final class AnyRequirement(RequirementCollection): """ A requirement that is met if any of the given requirements are met. """
[docs] def __init__(self, *requirements: Requirement | None): super().__init__(*requirements) self._summary = _("One or more of these requirements must be met")
[docs] @override def is_met(self) -> bool: return any(requirement.is_met() for requirement in self._requirements)
[docs] @override def summary(self) -> Localizable: return self._summary
[docs] class AllRequirements(RequirementCollection): """ A requirement that is met if all of the given requirements are met. """
[docs] def __init__(self, *requirements: Requirement | None): super().__init__(*requirements) self._summary = _("All of these requirements must be met")
[docs] @override def is_met(self) -> bool: return all(requirement.is_met() for requirement in self._requirements)
[docs] @override def summary(self) -> Localizable: return self._summary