Source code for betty.project.extension

"""Provide Betty's extension API."""

from __future__ import annotations

from typing import TypeVar, TYPE_CHECKING, Generic, Self, Sequence

from typing_extensions import override

from betty.config import Configuration, DefaultConfigurable
from betty.core import CoreComponent
from betty.locale.localizable import Localizable, _, call
from betty.plugin import (
    PluginRepository,
    PluginIdToTypeMapping,
    OrderedPlugin,
    CyclicDependencyError,
    DependentPlugin,
    sort_dependent_plugin_graph,
    sort_ordered_plugin_graph,
)
from betty.plugin.entry_point import EntryPointPluginRepository
from betty.project.factory import ProjectDependentFactory
from betty.requirement import AllRequirements
from betty.typing import private

if TYPE_CHECKING:
    from graphlib import TopologicalSorter
    from collections.abc import Iterable
    from betty.event_dispatcher import EventHandlerRegistry
    from betty.requirement import Requirement
    from betty.project import Project
    from pathlib import Path

_ConfigurationT = TypeVar("_ConfigurationT", bound=Configuration)


[docs] class Extension( OrderedPlugin["Extension"], DependentPlugin["Extension"], CoreComponent, ProjectDependentFactory, ): """ Integrate optional functionality with Betty :py:class:`betty.project.Project`s. Read more about :doc:`/development/plugin/extension`. To test your own subclasses, use :py:class:`betty.test_utils.project.extension.ExtensionTestBase`. """
[docs] def __init__(self, project: Project): assert type(self) is not Extension super().__init__() self._project = project
[docs] @override @classmethod async def new_for_project(cls, project: Project) -> Self: return cls(project)
[docs] def register_event_handlers(self, registry: EventHandlerRegistry) -> None: """ Register event handlers with the project. """ pass
@property def project(self) -> Project: """ The project this extension runs within. """ return self._project
[docs] @classmethod async def requirement(cls) -> Requirement: """ Define the requirement for this extension to be enabled. This defaults to the extension's dependencies. """ return await Dependencies.new(cls)
[docs] @classmethod def assets_directory_path(cls) -> Path | None: """ Return the path on disk where the extension's assets are located. This may be anywhere in your Python package. """ return None
_ExtensionT = TypeVar("_ExtensionT", bound=Extension) EXTENSION_REPOSITORY: PluginRepository[Extension] = EntryPointPluginRepository( "betty.extension" ) """ The project extension plugin repository. Read more about :doc:`/development/plugin/extension`. """
[docs] class Theme(Extension): """ An extension that is a front-end theme. """ pass # pragma: no cover
[docs] class ConfigurableExtension( DefaultConfigurable[_ConfigurationT], Extension, Generic[_ConfigurationT] ): """ A configurable extension. """
[docs] @override @classmethod async def new_for_project(cls, project: Project) -> Self: return cls(project, configuration=cls.new_default_configuration())
[docs] async def sort_extension_type_graph( sorter: TopologicalSorter[type[Extension]], extension_types: Iterable[type[Extension]], ) -> None: """ Sort an extension graph. """ await sort_dependent_plugin_graph(sorter, EXTENSION_REPOSITORY, extension_types) await sort_ordered_plugin_graph(sorter, EXTENSION_REPOSITORY, extension_types)
[docs] class Dependencies(AllRequirements): """ Check a dependent's dependency requirements. """
[docs] @private def __init__( self, dependent: type[Extension], extension_id_to_type_mapping: PluginIdToTypeMapping[Extension], dependency_requirements: Sequence[Requirement], ): super().__init__(*dependency_requirements) self._dependent = dependent self._extension_id_to_type_mapping = extension_id_to_type_mapping
[docs] @classmethod async def new(cls, dependent: type[Extension]) -> Self: """ Create a new instance. """ try: dependency_requirements = [ await ( await EXTENSION_REPOSITORY.get(dependency_identifier) if isinstance(dependency_identifier, str) else dependency_identifier ).requirement() for dependency_identifier in dependent.depends_on() ] except RecursionError: raise CyclicDependencyError([dependent]) from None else: return cls( dependent, await EXTENSION_REPOSITORY.mapping(), dependency_requirements )
[docs] @override def summary(self) -> Localizable: return _("{dependent_label} requires {dependency_labels}.").format( dependent_label=self._dependent.plugin_label(), dependency_labels=call( lambda localizer: ", ".join( self._extension_id_to_type_mapping[dependency_identifier] .plugin_label() .localize(localizer) for dependency_identifier in self._dependent.depends_on() ), ), )