From 416d8178116f484d80eb22ced100ea3680d0bd3d Mon Sep 17 00:00:00 2001 From: Bart Feenstra Date: Thu, 29 Aug 2024 16:56:37 +0100 Subject: [PATCH] Remove file operations from renderers --- betty/jinja2/__init__.py | 30 ++++++++-- betty/render.py | 58 +++++++++++++++---- documentation/development/plugin/renderer.rst | 7 +-- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/betty/jinja2/__init__.py b/betty/jinja2/__init__.py index ff1a607eb..7ebec856b 100644 --- a/betty/jinja2/__init__.py +++ b/betty/jinja2/__init__.py @@ -10,7 +10,17 @@ Mapping, ) from threading import Lock -from typing import Callable, Any, cast, TYPE_CHECKING, TypeAlias, final, Awaitable, Self +from typing import ( + Callable, + Any, + cast, + TYPE_CHECKING, + TypeAlias, + final, + Awaitable, + Self, + TypeVar, +) import aiofiles from aiofiles import os as aiofiles_os @@ -34,9 +44,8 @@ from betty.locale.localizable import Localizable, plain from betty.locale.localizer import DEFAULT_LOCALIZER from betty.locale.localizer import Localizer -from betty.plugin import Plugin from betty.project.factory import ProjectDependentFactory -from betty.render import Renderer +from betty.render import RendererPlugin, MediaTypey from betty.serde.dump import Dumpable, DumpMapping, VoidableDump, Dump from betty.typing import Void @@ -54,6 +63,8 @@ MutableSequence, ) +_MediaTypeyT = TypeVar("_MediaTypeyT", bound=MediaTypey) + def context_project(context: Context) -> Project: """ @@ -396,7 +407,7 @@ def _init_extensions(self) -> None: @final -class Jinja2Renderer(Renderer, ProjectDependentFactory, Plugin): +class Jinja2Renderer(RendererPlugin, ProjectDependentFactory): """ Render content as Jinja2 templates. """ @@ -425,6 +436,17 @@ def new_for_project(cls, project: Project) -> Self: def file_extensions(self) -> set[str]: return {".j2"} + @override + async def render( + self, + content: str, + media_typey: _MediaTypeyT, + *, + job_context: Context | None = None, + localizer: Localizer | None = None, + ) -> tuple[str, _MediaTypeyT]: + pass + @override async def render_file( self, diff --git a/betty/render.py b/betty/render.py index 45cb0f870..cc204e4a0 100644 --- a/betty/render.py +++ b/betty/render.py @@ -5,54 +5,88 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import final, TYPE_CHECKING +from pathlib import Path +from typing import final, TYPE_CHECKING, Union, TypeAlias +from mypy.graph_utils import TypeVar from typing_extensions import override +from betty.media_type import MediaType +from betty.plugin import Plugin from betty.plugin.entry_point import EntryPointPluginRepository from betty.typing import internal if TYPE_CHECKING: - from betty.plugin import PluginRepository, Plugin + from betty.plugin import PluginRepository from betty.locale.localizer import Localizer from betty.job import Context - from pathlib import Path from collections.abc import Sequence +MediaTypey: TypeAlias = Union[MediaType, Path] +_MediaTypeyT = TypeVar("_MediaTypeyT", bound=MediaTypey) + + class Renderer(ABC): """ Render content to HTML. - Read more about :doc:`/development/plugin/renderer`. + See also :py:class:`betty.render.RendererPlugin`. """ + @property + @abstractmethod + def media_types(self) -> set[MediaType]: + """ + The media types this renderer can render. + """ + pass + @property @abstractmethod def file_extensions(self) -> set[str]: """ - The extensions of the files this renderer can render. + The extensions (including leading dot) of the files this renderer can render. """ pass + # @todo How truthfully can we return a changed media type? + # @todo With file names, we can resolve nested extensions, and therefore nested renderers + # @todo But also, we kind of assume each renderer returns HTML. + # @todo So, maybe, no longer allow renderers to be chained. + # @todo However, we do a few *.json.j2 files still. We'd have to convert those to Python code first + # @todo and then restrict renderers to HTML only. @abstractmethod - async def render_file( + async def render( self, - file_path: Path, + content: str, + media_typey: _MediaTypeyT, *, job_context: Context | None = None, localizer: Localizer | None = None, - ) -> Path: + ) -> tuple[str, _MediaTypeyT]: """ - Render a single file. + Render content. - :return: The file's new path, which may have been changed, e.g. a - renderer-specific extension may have been stripped from the end. + :return: The rendered content, and its new media type or path, which may have been changed. """ pass -RENDERER_REPOSITORY: PluginRepository[Renderer & Plugin] = EntryPointPluginRepository( +class RendererPlugin(Renderer, Plugin): + """ + A renderer as a plugin. + + Read more about :doc:`/development/plugin/renderer`. + """ + + @override + @property + def media_types(self) -> set[MediaType]: + return {f"text/x.betty.{self.plugin_id()}"} + + +RENDERER_REPOSITORY: PluginRepository[RendererPlugin] = EntryPointPluginRepository( "betty.renderer" ) """ diff --git a/documentation/development/plugin/renderer.rst b/documentation/development/plugin/renderer.rst index a0bf989e0..d83ae16f8 100644 --- a/documentation/development/plugin/renderer.rst +++ b/documentation/development/plugin/renderer.rst @@ -15,16 +15,15 @@ Renderers convert textual content to HTML. A renderer is often built to support Creating a renderer ------------------- -#. Create a new class that extends both :py:class:`betty.render.Renderer` and :py:class:`betty.plugin.Plugin` and implements the abstract methods, +#. Create a new class that extends :py:class:`betty.render.RendererPlugin` and implements the abstract methods, for example: .. code-block:: python from typing import override - from betty.plugin import Plugin - from betty.render import Renderer + from betty.render import RendererPlugin - class MyRenderer(Renderer, Plugin): + class MyRenderer(RendererPlugin): @override @classmethod def plugin_id(cls) -> str: