Source code for betty.model.config

"""
Configuration for the data model.
"""

from __future__ import annotations

from typing import final, Generic, Iterable, TypeVar, TYPE_CHECKING

from typing_extensions import override

from betty.assertion import (
    assert_record,
    RequiredField,
    assert_or,
    assert_none,
    assert_setattr,
    OptionalField,
    assert_str,
)
from betty.assertion.error import AssertionFailed
from betty.config import Configuration
from betty.config.collections.sequence import ConfigurationSequence
from betty.locale.localizable import _
from betty.machine_name import MachineName, assert_machine_name
from betty.model import Entity
from betty.plugin import PluginIdentifier, resolve_identifier, PluginRepository
from betty.plugin.assertion import assert_plugin

if TYPE_CHECKING:
    from betty.serde.dump import Dump, DumpMapping


_EntityCoT = TypeVar("_EntityCoT", bound=Entity, covariant=True)


[docs] @final class EntityReference(Configuration, Generic[_EntityCoT]): """ Configuration that references an entity from the project's ancestry. """
[docs] def __init__( self, entity_type: PluginIdentifier[_EntityCoT] | None = None, entity_id: str | None = None, *, entity_type_is_constrained: bool = False, ): super().__init__() self._entity_type = ( None if entity_type is None else resolve_identifier(entity_type) ) self._entity_id = entity_id self._entity_type_is_constrained = entity_type_is_constrained
@property def entity_type(self) -> MachineName | None: """ The referenced entity's type. """ return self._entity_type @entity_type.setter def entity_type(self, entity_type: PluginIdentifier[_EntityCoT]) -> None: if self._entity_type_is_constrained: raise AttributeError( f"The entity type cannot be set, as it is already constrained to {self._entity_type}." ) self._entity_type = resolve_identifier(entity_type) @property def entity_id(self) -> str | None: """ The referenced entity's ID. """ return self._entity_id @entity_id.setter def entity_id(self, entity_id: str) -> None: self._entity_id = entity_id @entity_id.deleter def entity_id(self) -> None: self._entity_id = None @property def entity_type_is_constrained(self) -> bool: """ Whether the entity type may be changed. """ return self._entity_type_is_constrained
[docs] @override def load(self, dump: Dump) -> None: if isinstance(dump, dict) or not self.entity_type_is_constrained: assert_record( RequiredField( "entity_type", assert_or( assert_none(), assert_machine_name() | assert_setattr(self, "_entity_type"), ), ), OptionalField( "entity", assert_str() | assert_setattr(self, "entity_id"), ), )(dump) else: assert_str()(dump) assert_setattr(self, "entity_id")(dump)
[docs] @override def dump(self) -> DumpMapping[Dump] | str | None: if self.entity_type_is_constrained: return self.entity_id dump: DumpMapping[Dump] = {"entity_type": self.entity_type} if self.entity_id is not None: dump["entity"] = self.entity_id return dump
[docs] async def validate(self, entity_type_repository: PluginRepository[Entity]) -> None: """ Validate the configuration. """ assert_plugin(await entity_type_repository.mapping())(self.entity_type)
[docs] @final class EntityReferenceSequence( Generic[_EntityCoT], ConfigurationSequence[EntityReference[_EntityCoT]] ): """ Configuration for a sequence of references to entities from the project's ancestry. """
[docs] def __init__( self, entity_references: Iterable[EntityReference[_EntityCoT]] | None = None, *, entity_type_constraint: PluginIdentifier[_EntityCoT] | None = None, ): self._entity_type_constraint = ( None if entity_type_constraint is None else resolve_identifier(entity_type_constraint) ) super().__init__(entity_references)
@override def _load_item(self, dump: Dump) -> EntityReference[_EntityCoT]: configuration = EntityReference[_EntityCoT]( # Use a dummy entity type for now to satisfy the initializer. # It will be overridden when loading the dump. Entity # type: ignore[arg-type] if self._entity_type_constraint is None else self._entity_type_constraint, entity_type_is_constrained=self._entity_type_constraint is not None, ) configuration.load(dump) return configuration @override def _pre_add(self, configuration: EntityReference[_EntityCoT]) -> None: super()._pre_add(configuration) entity_type_constraint = self._entity_type_constraint entity_reference_entity_type = configuration._entity_type if entity_type_constraint is None: configuration._entity_type_is_constrained = False return configuration._entity_type_is_constrained = True if ( entity_reference_entity_type == entity_type_constraint and configuration.entity_type_is_constrained ): return if entity_reference_entity_type is None: raise AssertionFailed( _( "The entity reference must be for an entity of type {expected_entity_type_id}, but instead does not specify an entity type at all." ).format( expected_entity_type_id=entity_type_constraint, ) ) raise AssertionFailed( _( "The entity reference must be for an entity of type {expected_entity_type_id}, but instead is for an entity of type {actual_entity_type_id}." ).format( expected_entity_type_id=entity_type_constraint, actual_entity_type_id=entity_reference_entity_type, ) )
[docs] async def validate(self, entity_type_repository: PluginRepository[Entity]) -> None: """ Validate the configuration. """ for reference in self: await reference.validate(entity_type_repository)