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 betty.asyncio import wait_to_thread
from betty.error import UserFacingError
from betty.locale.localizable import _, Localizable
from typing_extensions import override

if TYPE_CHECKING:
    from betty.locale.localizer import Localizer


[docs] class Requirement(Localizable): """ Express a requirement. """
[docs] @abstractmethod async def is_met(self) -> bool: """ Check if the requirement is met. """ pass
[docs] async def assert_met(self) -> None: """ Assert that the requirement is met. """ if not await self.is_met(): raise RequirementError(self) return None
[docs] @abstractmethod async def summary(self) -> Localizable: """ Get the requirement's human-readable summary. """ pass
[docs] async def details(self) -> Localizable | None: """ Get the requirement's human-readable additional details. """ return None
[docs] @override def localize(self, localizer: Localizer) -> str: string = wait_to_thread(self.summary()).localize(localizer) details = wait_to_thread(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] @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: list[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) -> str: localized = super().localize(localizer) for requirement in self._requirements: localized += f'\n-{indent(requirement.localize(localizer), " ")[1:]}' return localized
[docs] @override 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] @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 async def is_met(self) -> bool: return any([await requirement.is_met() for requirement in self._requirements])
[docs] @override async 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 async def is_met(self) -> bool: return all([await requirement.is_met() for requirement in self._requirements])
[docs] @override async def summary(self) -> Localizable: return self._summary