From b0b0c50c687986b513724f4942933e00a857af13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= <25355197+provinzkraut@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:54:46 +0100 Subject: [PATCH] Add 'SerializationPlugin' and 'InitPlugin' --- docs/examples/plugins/init_plugin_protocol.py | 4 +- docs/usage/plugins/index.rst | 19 ++--- litestar/_openapi/plugin.py | 4 +- litestar/channels/plugin.py | 4 +- litestar/contrib/opentelemetry/plugin.py | 4 +- litestar/plugins/__init__.py | 4 + litestar/plugins/base.py | 84 ++++++++++++++++++- litestar/plugins/flash.py | 4 +- litestar/plugins/problem_details.py | 4 +- litestar/plugins/pydantic/__init__.py | 4 +- litestar/plugins/pydantic/plugins/init.py | 4 +- litestar/plugins/structlog.py | 4 +- tests/unit/test_plugins/test_base.py | 4 +- 13 files changed, 112 insertions(+), 35 deletions(-) diff --git a/docs/examples/plugins/init_plugin_protocol.py b/docs/examples/plugins/init_plugin_protocol.py index 84678c1ead..d532e901ad 100644 --- a/docs/examples/plugins/init_plugin_protocol.py +++ b/docs/examples/plugins/init_plugin_protocol.py @@ -3,7 +3,7 @@ from litestar import Litestar, get from litestar.config.app import AppConfig from litestar.di import Provide -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin @get("/", sync_to_thread=False) @@ -15,7 +15,7 @@ def get_name() -> str: return "world" -class MyPlugin(InitPluginProtocol): +class MyPlugin(InitPlugin): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.dependencies["name"] = Provide(get_name, sync_to_thread=False) app_config.route_handlers.append(route_handler) diff --git a/docs/usage/plugins/index.rst b/docs/usage/plugins/index.rst index 5eb27c9d32..751ec98566 100644 --- a/docs/usage/plugins/index.rst +++ b/docs/usage/plugins/index.rst @@ -13,23 +13,16 @@ Litestar supports a plugin system that allows you to extend the functionality of Plugins are defined by protocols, and any type that satisfies a protocol can be included in the ``plugins`` argument of the :class:`app `. -The following plugin protocols are defined. -1. :class:`InitPluginProtocol `: This protocol defines a contract for plugins -that can interact with the data that is used to instantiate the application instance. +InitPlugin +---------- -2. :class:`SerializationPluginProtocol `: This protocol defines -the contract for plugins that extend serialization functionality of the application. - -InitPluginProtocol ------------------- - -``InitPluginProtocol`` defines an interface that allows for customization of the application's initialization process. +``InitPlugin`` defines an interface that allows for customization of the application's initialization process. Init plugins can define dependencies, add route handlers, configure middleware, and much more! Implementations of these plugins must define a single method: -:meth:`on_app_init(self, app_config: AppConfig) -> AppConfig: ` +:meth:`on_app_init(self, app_config: AppConfig) -> AppConfig: ` ---------------------------------------------------------------------------------------------------------------- The method accepts and must return an :class:`AppConfig ` instance, which can be modified @@ -46,9 +39,9 @@ The following example shows a simple plugin that adds a route handler, and a dep .. literalinclude:: /examples/plugins/init_plugin_protocol.py :language: python - :caption: ``InitPluginProtocol`` implementation example + :caption: ``InitPlugin`` implementation example -The ``MyPlugin`` class is an implementation of the :class:`InitPluginProtocol `. It +The ``MyPlugin`` class is an implementation of the :class:`InitPlugin `. It defines a single method, ``on_app_init()``, which takes an :class:`AppConfig ` instance as an argument and returns same. diff --git a/litestar/_openapi/plugin.py b/litestar/_openapi/plugin.py index 78349b7e3f..4ba68d9ab4 100644 --- a/litestar/_openapi/plugin.py +++ b/litestar/_openapi/plugin.py @@ -9,7 +9,7 @@ from litestar.exceptions import ImproperlyConfiguredException, NotFoundException from litestar.handlers import get from litestar.openapi.plugins import JsonRenderPlugin -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin from litestar.plugins.base import ReceiveRoutePlugin from litestar.response import Response from litestar.router import Router @@ -51,7 +51,7 @@ def handle_schema_path_not_found(path: str = "/") -> Response: return Response(content, media_type=MediaType.HTML, status_code=HTTP_404_NOT_FOUND) -class OpenAPIPlugin(InitPluginProtocol, ReceiveRoutePlugin): +class OpenAPIPlugin(InitPlugin, ReceiveRoutePlugin): __slots__ = ( "_openapi", "_openapi_config", diff --git a/litestar/channels/plugin.py b/litestar/channels/plugin.py index 59884454d4..5cab3097cd 100644 --- a/litestar/channels/plugin.py +++ b/litestar/channels/plugin.py @@ -11,7 +11,7 @@ from litestar.di import Provide from litestar.exceptions import ImproperlyConfiguredException, LitestarException from litestar.handlers import WebsocketRouteHandler -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin from litestar.serialization import default_serializer from .subscriber import BacklogStrategy, EventCallback, Subscriber @@ -30,7 +30,7 @@ class ChannelsException(LitestarException): pass -class ChannelsPlugin(InitPluginProtocol, AbstractAsyncContextManager): +class ChannelsPlugin(InitPlugin, AbstractAsyncContextManager): def __init__( self, backend: ChannelsBackend, diff --git a/litestar/contrib/opentelemetry/plugin.py b/litestar/contrib/opentelemetry/plugin.py index 6d5f34677c..1b9a3bbcd7 100644 --- a/litestar/contrib/opentelemetry/plugin.py +++ b/litestar/contrib/opentelemetry/plugin.py @@ -5,14 +5,14 @@ from litestar.contrib.opentelemetry.config import OpenTelemetryConfig from litestar.contrib.opentelemetry.middleware import OpenTelemetryInstrumentationMiddleware from litestar.middleware.base import DefineMiddleware -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin if TYPE_CHECKING: from litestar.config.app import AppConfig from litestar.types.composite_types import Middleware -class OpenTelemetryPlugin(InitPluginProtocol): +class OpenTelemetryPlugin(InitPlugin): """OpenTelemetry Plugin.""" __slots__ = ("_middleware", "config") diff --git a/litestar/plugins/__init__.py b/litestar/plugins/__init__.py index 39421e7d30..6f32492995 100644 --- a/litestar/plugins/__init__.py +++ b/litestar/plugins/__init__.py @@ -2,11 +2,13 @@ CLIPlugin, CLIPluginProtocol, DIPlugin, + InitPlugin, InitPluginProtocol, OpenAPISchemaPlugin, OpenAPISchemaPluginProtocol, PluginProtocol, PluginRegistry, + SerializationPlugin, SerializationPluginProtocol, ) @@ -14,10 +16,12 @@ "CLIPlugin", "CLIPluginProtocol", "DIPlugin", + "InitPlugin", "InitPluginProtocol", "OpenAPISchemaPlugin", "OpenAPISchemaPluginProtocol", "PluginProtocol", "PluginRegistry", + "SerializationPlugin", "SerializationPluginProtocol", ) diff --git a/litestar/plugins/base.py b/litestar/plugins/base.py index c043fbd7cf..9a17703631 100644 --- a/litestar/plugins/base.py +++ b/litestar/plugins/base.py @@ -21,18 +21,24 @@ "CLIPlugin", "CLIPluginProtocol", "DIPlugin", + "InitPlugin", "InitPluginProtocol", "OpenAPISchemaPlugin", "OpenAPISchemaPluginProtocol", "PluginProtocol", "PluginRegistry", + "SerializationPlugin", "SerializationPluginProtocol", ) @runtime_checkable class InitPluginProtocol(Protocol): - """Protocol used to define plugins that affect the application's init process.""" + """Protocol used to define plugins that affect the application's init process. + + .. deprecated:: 2.15 + Use 'InitPlugin' instead + """ __slots__ = () @@ -74,6 +80,47 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig: return app_config # pragma: no cover +class InitPlugin(InitPluginProtocol): + """Protocol used to define plugins that affect the application's init process.""" + + def on_app_init(self, app_config: AppConfig) -> AppConfig: + """Receive the :class:`AppConfig<.config.app.AppConfig>` instance after `on_app_init` hooks have been called. + + Examples: + .. code-block:: python + + from litestar import Litestar, get + from litestar.di import Provide + from litestar.plugins import InitPluginProtocol + + + def get_name() -> str: + return "world" + + + @get("/my-path") + def my_route_handler(name: str) -> dict[str, str]: + return {"hello": name} + + + class MyPlugin(InitPluginProtocol): + def on_app_init(self, app_config: AppConfig) -> AppConfig: + app_config.dependencies["name"] = Provide(get_name) + app_config.route_handlers.append(my_route_handler) + return app_config + + + app = Litestar(plugins=[MyPlugin()]) + + Args: + app_config: The :class:`AppConfig ` instance. + + Returns: + The app config object. + """ + return app_config # pragma: no cover + + class ReceiveRoutePlugin: """Receive routes as they are added to the application.""" @@ -128,7 +175,12 @@ def server_lifespan(self, app: Litestar) -> Iterator[None]: @runtime_checkable class SerializationPluginProtocol(Protocol): - """Protocol used to define a serialization plugin for DTOs.""" + """Protocol used to define a serialization plugin for DTOs. + + .. deprecated:: 2.15 + Use 'litestar.plugins.SerializationPluginProtocol' instead + + """ __slots__ = () @@ -155,6 +207,34 @@ def create_dto_for_type(self, field_definition: FieldDefinition) -> type[Abstrac raise NotImplementedError() +class SerializationPlugin(SerializationPluginProtocol, abc.ABC): + """Abstract base class for plugins that extend DTO functionality""" + + @abc.abstractmethod + def supports_type(self, field_definition: FieldDefinition) -> bool: + """Given a value of indeterminate type, determine if this value is supported by the plugin. + + Args: + field_definition: A parsed type. + + Returns: + Whether the type is supported by the plugin. + """ + raise NotImplementedError() + + @abc.abstractmethod + def create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: + """Given a parsed type, create a DTO class. + + Args: + field_definition: A parsed type. + + Returns: + A DTO class. + """ + raise NotImplementedError() + + class DIPlugin(abc.ABC): """Extend dependency injection""" diff --git a/litestar/plugins/flash.py b/litestar/plugins/flash.py index 9062474d07..69e483a207 100644 --- a/litestar/plugins/flash.py +++ b/litestar/plugins/flash.py @@ -10,7 +10,7 @@ from litestar.exceptions import MissingDependencyException from litestar.middleware import DefineMiddleware from litestar.middleware.session import SessionMiddleware -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin from litestar.security.session_auth.middleware import MiddlewareWrapper from litestar.template.base import _get_request_from_context from litestar.utils.predicates import is_class_and_subclass @@ -30,7 +30,7 @@ class FlashConfig: template_config: TemplateConfig -class FlashPlugin(InitPluginProtocol): +class FlashPlugin(InitPlugin): """Flash messages Plugin.""" def __init__(self, config: FlashConfig): diff --git a/litestar/plugins/problem_details.py b/litestar/plugins/problem_details.py index 989b298e60..7dbdd81ab7 100644 --- a/litestar/plugins/problem_details.py +++ b/litestar/plugins/problem_details.py @@ -8,7 +8,7 @@ from typing_extensions import TypeAlias from litestar.exceptions.http_exceptions import HTTPException -from litestar.plugins.base import InitPluginProtocol +from litestar.plugins.base import InitPlugin from litestar.response.base import Response if TYPE_CHECKING: @@ -134,7 +134,7 @@ class ProblemDetailsConfig: """ -class ProblemDetailsPlugin(InitPluginProtocol): +class ProblemDetailsPlugin(InitPlugin): """A plugin to convert exceptions into problem details as per RFC 9457.""" def __init__(self, config: ProblemDetailsConfig | None = None): diff --git a/litestar/plugins/pydantic/__init__.py b/litestar/plugins/pydantic/__init__.py index 6b275edf3a..0e168fd532 100644 --- a/litestar/plugins/pydantic/__init__.py +++ b/litestar/plugins/pydantic/__init__.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin from litestar.plugins.pydantic.dto import PydanticDTO from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin @@ -40,7 +40,7 @@ def _model_dump_json(model: BaseModel | BaseModelV1, by_alias: bool = False) -> ) -class PydanticPlugin(InitPluginProtocol): +class PydanticPlugin(InitPlugin): """A plugin that provides Pydantic integration.""" __slots__ = ( diff --git a/litestar/plugins/pydantic/plugins/init.py b/litestar/plugins/pydantic/plugins/init.py index fb3a31d824..dfa90c47c9 100644 --- a/litestar/plugins/pydantic/plugins/init.py +++ b/litestar/plugins/pydantic/plugins/init.py @@ -10,7 +10,7 @@ from litestar._signature.types import ExtendedMsgSpecValidationError from litestar.exceptions import MissingDependencyException -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin from litestar.plugins.pydantic.utils import is_pydantic_v2 from litestar.utils import is_class_and_subclass @@ -122,7 +122,7 @@ def is_pydantic_v2_model_class(annotation: Any) -> TypeGuard[type[pydantic_v2.Ba return is_class_and_subclass(annotation, pydantic_v2.BaseModel) # pyright: ignore[reportOptionalMemberAccess] -class PydanticInitPlugin(InitPluginProtocol): +class PydanticInitPlugin(InitPlugin): __slots__ = ( "exclude", "exclude_defaults", diff --git a/litestar/plugins/structlog.py b/litestar/plugins/structlog.py index fafa3dde8f..792479067c 100644 --- a/litestar/plugins/structlog.py +++ b/litestar/plugins/structlog.py @@ -6,7 +6,7 @@ from litestar.cli._utils import console from litestar.logging.config import StructLoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig -from litestar.plugins import InitPluginProtocol +from litestar.plugins import InitPlugin if TYPE_CHECKING: from litestar.config.app import AppConfig @@ -22,7 +22,7 @@ class StructlogConfig: """Enable request logging.""" -class StructlogPlugin(InitPluginProtocol): +class StructlogPlugin(InitPlugin): """Structlog Plugin.""" __slots__ = ("_config",) diff --git a/tests/unit/test_plugins/test_base.py b/tests/unit/test_plugins/test_base.py index c8d06658c8..1686dd0dba 100644 --- a/tests/unit/test_plugins/test_base.py +++ b/tests/unit/test_plugins/test_base.py @@ -7,7 +7,7 @@ from litestar import Litestar, MediaType, get from litestar.constants import UNDEFINED_SENTINELS -from litestar.plugins import CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPlugin, PluginRegistry +from litestar.plugins import CLIPluginProtocol, InitPlugin, OpenAPISchemaPlugin, PluginRegistry from litestar.plugins.attrs import AttrsSchemaPlugin from litestar.plugins.core import MsgspecDIPlugin from litestar.plugins.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin @@ -29,7 +29,7 @@ def greet() -> str: def on_startup(app: Litestar) -> None: app.state.called = True - class PluginWithInitOnly(InitPluginProtocol): + class PluginWithInitOnly(InitPlugin): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.tags.append(tag) app_config.on_startup.append(on_startup)