Skip to content

Commit

Permalink
Add 'SerializationPlugin' and 'InitPlugin'
Browse files Browse the repository at this point in the history
  • Loading branch information
provinzkraut committed Feb 25, 2025
1 parent 6054667 commit 9df7d92
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 34 deletions.
4 changes: 2 additions & 2 deletions docs/examples/plugins/init_plugin_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
19 changes: 6 additions & 13 deletions docs/usage/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <litestar.app.Litestar>`.

The following plugin protocols are defined.

1. :class:`InitPluginProtocol <litestar.plugins.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 <litestar.plugins.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: <litestar.plugins.InitPluginProtocol.on_app_init>`
:meth:`on_app_init(self, app_config: AppConfig) -> AppConfig: <litestar.plugins.InitPlugin.on_app_init>`
----------------------------------------------------------------------------------------------------------------

The method accepts and must return an :class:`AppConfig <litestar.config.app.AppConfig>` instance, which can be modified
Expand All @@ -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 <litestar.plugins.InitPluginProtocol>`. It
The ``MyPlugin`` class is an implementation of the :class:`InitPlugin <litestar.plugins.InitPlugin>`. It
defines a single method, ``on_app_init()``, which takes an :class:`AppConfig <litestar.config.app.AppConfig>` instance
as an argument and returns same.

Expand Down
4 changes: 2 additions & 2 deletions litestar/_openapi/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions litestar/channels/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +30,7 @@ class ChannelsException(LitestarException):
pass


class ChannelsPlugin(InitPluginProtocol, AbstractAsyncContextManager):
class ChannelsPlugin(InitPlugin, AbstractAsyncContextManager):
def __init__(
self,
backend: ChannelsBackend,
Expand Down
4 changes: 2 additions & 2 deletions litestar/contrib/opentelemetry/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions litestar/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
CLIPlugin,
CLIPluginProtocol,
DIPlugin,
InitPlugin,
InitPluginProtocol,
OpenAPISchemaPlugin,
OpenAPISchemaPluginProtocol,
PluginProtocol,
PluginRegistry,
SerializationPlugin,
SerializationPluginProtocol,
)

__all__ = (
"CLIPlugin",
"CLIPluginProtocol",
"DIPlugin",
"InitPlugin",
"InitPluginProtocol",
"OpenAPISchemaPlugin",
"OpenAPISchemaPluginProtocol",
"PluginProtocol",
"PluginRegistry",
"SerializationPlugin",
"SerializationPluginProtocol",
)
84 changes: 83 additions & 1 deletion litestar/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,66 @@
"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.
.. deprecated:: 2.15
Use 'InitPlugin' instead
"""

__slots__ = ()

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 <litestar.config.app.AppConfig>` instance.
Returns:
The app config object.
"""
return app_config # pragma: no cover


class InitPlugin(InitPluginProtocol):
"""Protocol used to define plugins that affect the application's init process."""

__slots__ = ()
Expand Down Expand Up @@ -128,7 +177,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__ = ()

Expand All @@ -155,6 +209,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"""

Expand Down
4 changes: 2 additions & 2 deletions litestar/plugins/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +30,7 @@ class FlashConfig:
template_config: TemplateConfig


class FlashPlugin(InitPluginProtocol):
class FlashPlugin(InitPlugin):
"""Flash messages Plugin."""

def __init__(self, config: FlashConfig):
Expand Down
4 changes: 2 additions & 2 deletions litestar/plugins/problem_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions litestar/plugins/pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__ = (
Expand Down
4 changes: 2 additions & 2 deletions litestar/plugins/pydantic/plugins/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions litestar/plugins/structlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,7 +22,7 @@ class StructlogConfig:
"""Enable request logging."""


class StructlogPlugin(InitPluginProtocol):
class StructlogPlugin(InitPlugin):
"""Structlog Plugin."""

__slots__ = ("_config",)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_plugins/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit 9df7d92

Please sign in to comment.