betty.project.extension - Betty documentation

Hide navigation sidebar
Hide table of contents sidebar
Skip to content
Back to top
Toggle table of contents sidebar

# 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.service import ServiceProvider
    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)

    class Extension(
        OrderedPlugin["Extension"],
        DependentPlugin["Extension"],
        ServiceProvider,
        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`.
        """

        def __init__(self, project: Project):
            assert type(self) is not Extension
            super().__init__()
            self._project = project

        @override
        @classmethod
        async def new_for_project(cls, project: Project) -> Self:
            return cls(project)

        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

        @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)

        @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`.
    """

    class Theme(Extension):
        """
        An extension that is a front-end theme.
        """

        pass  # pragma: no cover

    class ConfigurableExtension(
        DefaultConfigurable[_ConfigurationT], Extension, Generic[_ConfigurationT]
    ):
        """
        A configurable extension.
        """

        @override
        @classmethod
        async def new_for_project(cls, project: Project) -> Self:
            return cls(project, configuration=cls.new_default_configuration())

    async def sort_extension_type_graph(
        sorter: TopologicalSorter[type[Extension]],
        extension_types: Iterable[type[Extension]],
    ) -> None:
        """
        Sort an extension graph.
        """
        await sort_ordered_plugin_graph(
            sorter,
            EXTENSION_REPOSITORY,
            await sort_dependent_plugin_graph(
                sorter, EXTENSION_REPOSITORY, extension_types
            ),
        )

    class Dependencies(AllRequirements):
        """
        Check a dependent's dependency requirements.
        """

        @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

        @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
                )

        @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()
                    ),
                ),
            )