Skip to content

Commit

Permalink
feat(plugins)!: Remove CLIPluginProtocol (#4027)
Browse files Browse the repository at this point in the history
  • Loading branch information
provinzkraut authored Feb 26, 2025
1 parent 911ae03 commit f635625
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 111 deletions.
4 changes: 2 additions & 2 deletions docs/release-notes/2.x-changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2135,8 +2135,8 @@
:type: feature
:pr: 2066

A new plugin protocol :class:`~litestar.plugins.CLIPluginProtocol` has been
added that can be used to extend the Litestar CLI.
A new plugin protocol ``CLIPluginProtocol`` has been added that can be used to
extend the Litestar CLI.

.. seealso::
:ref:`usage/cli:Using a plugin`
Expand Down
14 changes: 12 additions & 2 deletions docs/release-notes/whats-new-3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,21 @@ methods and properties have been moved from ``Router`` to ``Litestar``:
- ``routes``


Removal of ``CLIPluginProtocol``
---------------------------------


The :class:`~typing.Protocol` ``CLIPluginProtocol`` has been removed in favour
of the abstract ``CLIPluginProtocol``. The functionality and interface remain the same,
the only difference being that plugins that wish to provide this functionality are now
required to inherit from :class:`~.plugins.CLIPlugin`.


Removal of ``OpenAPISchemaPluginProtocol``
------------------------------------------

The :class:`~typing.Protocol` ``OpenAPISchemaPluginProtocol`` has been removed in favour
of the abstract :class:`~litestar.plugins.OpenAPISchemaPlugin`. The functionality and
interface remain the same, the only difference is that plugins that wish to provide this
functionality are now required to inherit from
interface remain the same, the only difference being that plugins that wish to provide
this functionality are now required to inherit from
:class:`~.plugins.OpenAPISchemaPlugin`.
4 changes: 2 additions & 2 deletions docs/tutorials/sqlalchemy/0-introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ serializable by Litestar.
/examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py
:language: python
:linenos:
:lines: 2-3,14-15,47-50,91-97
:emphasize-lines: 3,4,6,7,10,15
:lines: 2-3,14-15,47-50,91-98
:emphasize-lines: 3,6,10,15

Behavior
++++++++
Expand Down
12 changes: 6 additions & 6 deletions docs/usage/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Extending the CLI

Litestar's CLI is built with `click <https://click.palletsprojects.com/>`_ and can be extended by making use of
`entry points <https://packaging.python.org/en/latest/specifications/entry-points/>`_,
or by creating a plugin that conforms to the :class:`~.plugins.CLIPluginProtocol`.
or by creating a plugin inheriting from :class:`~.plugins.CLIPlugin`.

Using entry points
^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -103,26 +103,26 @@ entries should point to a :class:`click.Command` or :class:`click.Group`:
Using a plugin
^^^^^^^^^^^^^^

A plugin extending the CLI can be created using the :class:`~.plugins.CLIPluginProtocol`.
Its :meth:`~.plugins.CLIPluginProtocol.on_cli_init` will be called during the initialization of the CLI,
A plugin extending the CLI can be created using the :class:`~.plugins.CLIPlugin`.
Its :meth:`~.plugins.CLIPlugin.on_cli_init` will be called during the initialization of the CLI,
and receive the root :class:`click.Group` as its first argument, which can then be used to add or override commands:

.. code-block:: python
:caption: Creating a CLI plugin
from litestar import Litestar
from litestar.plugins import CLIPluginProtocol
from litestar.plugins import CLIPlugin
from click import Group
class CLIPlugin(CLIPluginProtocol):
class MyPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
@cli.command()
def is_debug_mode(app: Litestar):
print(app.debug)
app = Litestar(plugins=[CLIPlugin()])
app = Litestar(plugins=[MyPlugin()])
Accessing the app instance
^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ our application.
:caption: SQLAlchemy Async Marking Fields
:language: python
:linenos:
:emphasize-lines: 9,23,28
:emphasize-lines: 10,23

.. tab-item:: Sync

.. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_serialization_plugin_marking_fields.py
:caption: SQLAlchemy Sync Marking Fields
:language: python
:linenos:
:emphasize-lines: 9,23,28
:emphasize-lines: 10,23

In the above example, a new attribute called ``super_secret_value`` has been added to the model, and a value set for it
in the handler. However, due to "marking" the field as "private", when the model is serialized, the value is not present
Expand Down
5 changes: 2 additions & 3 deletions litestar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,13 @@
from litestar.middleware._internal.cors import CORSMiddleware
from litestar.openapi.config import OpenAPIConfig
from litestar.plugins import (
CLIPluginProtocol,
CLIPlugin,
InitPluginProtocol,
OpenAPISchemaPlugin,
PluginProtocol,
PluginRegistry,
SerializationPluginProtocol,
)
from litestar.plugins.base import CLIPlugin
from litestar.router import Router
from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
from litestar.stores.registry import StoreRegistry
Expand Down Expand Up @@ -524,7 +523,7 @@ def _patch_opentelemetry_middleware(config: AppConfig) -> AppConfig:

@property
@deprecated(version="2.0", alternative="Litestar.plugins.cli", kind="property")
def cli_plugins(self) -> list[CLIPluginProtocol]:
def cli_plugins(self) -> list[CLIPlugin]:
return list(self.plugins.cli)

@property
Expand Down
2 changes: 0 additions & 2 deletions litestar/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from litestar.plugins.base import (
CLIPlugin,
CLIPluginProtocol,
DIPlugin,
InitPlugin,
InitPluginProtocol,
Expand All @@ -13,7 +12,6 @@

__all__ = (
"CLIPlugin",
"CLIPluginProtocol",
"DIPlugin",
"InitPlugin",
"InitPluginProtocol",
Expand Down
23 changes: 6 additions & 17 deletions litestar/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

__all__ = (
"CLIPlugin",
"CLIPluginProtocol",
"DIPlugin",
"InitPlugin",
"InitPluginProtocol",
Expand Down Expand Up @@ -132,11 +131,8 @@ def receive_route(self, route: BaseRoute) -> None:
"""Receive routes as they are registered on an application."""


@runtime_checkable
class CLIPluginProtocol(Protocol):
"""Plugin protocol to extend the CLI."""

__slots__ = ()
class CLIPlugin:
"""Plugin protocol to extend the CLI Server Lifespan."""

def on_cli_init(self, cli: Group) -> None:
"""Called when the CLI is initialized.
Expand All @@ -150,11 +146,11 @@ def on_cli_init(self, cli: Group) -> None:
.. code-block:: python
from litestar import Litestar
from litestar.plugins import CLIPluginProtocol
from litestar.plugins import CLIPlugin
from click import Group
class CLIPlugin(CLIPluginProtocol):
class CLIPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
@cli.command()
def is_debug_mode(app: Litestar):
Expand All @@ -164,12 +160,6 @@ def is_debug_mode(app: Litestar):
app = Litestar(plugins=[CLIPlugin()])
"""


class CLIPlugin(CLIPluginProtocol):
"""Plugin protocol to extend the CLI Server Lifespan."""

__slots__ = ()

@contextmanager
def server_lifespan(self, app: Litestar) -> Iterator[None]:
yield
Expand Down Expand Up @@ -320,7 +310,6 @@ def is_constrained_field(field_definition: FieldDefinition) -> bool:

PluginProtocol = Union[
CLIPlugin,
CLIPluginProtocol,
InitPluginProtocol,
OpenAPISchemaPlugin,
ReceiveRoutePlugin,
Expand All @@ -337,7 +326,7 @@ class PluginRegistry:
"openapi": "Plugins that implement the OpenAPISchemaPluginProtocol",
"receive_route": "ReceiveRoutePlugin instances",
"serialization": "Plugins that implement the SerializationPluginProtocol",
"cli": "Plugins that implement the CLIPluginProtocol",
"cli": "Plugins that implement the CLIPlugin",
"di": "DIPlugin instances",
"_plugins_by_type": None,
"_plugins": None,
Expand All @@ -351,7 +340,7 @@ def __init__(self, plugins: list[PluginProtocol]) -> None:
self.openapi = tuple(p for p in plugins if isinstance(p, OpenAPISchemaPlugin))
self.receive_route = tuple(p for p in plugins if isinstance(p, ReceiveRoutePlugin))
self.serialization = tuple(p for p in plugins if isinstance(p, SerializationPluginProtocol))
self.cli = tuple(p for p in plugins if isinstance(p, CLIPluginProtocol))
self.cli = tuple(p for p in plugins if isinstance(p, CLIPlugin))
self.di = tuple(p for p in plugins if isinstance(p, DIPlugin))

def get(self, type_: type[PluginT] | str) -> PluginT:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pydantic = [
]
redis = ["redis[hiredis]>=4.4.4"]
valkey = ["valkey[libvalkey]>=6.0.2"]
sqlalchemy = ["advanced-alchemy>=0.2.2"]
sqlalchemy = ["advanced-alchemy>=0.32.1"]
standard = ["jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'", "fast-query-parsers>=1.0.2"]
structlog = ["structlog"]

Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
NotFoundException,
)
from litestar.logging.config import LoggingConfig
from litestar.plugins import CLIPluginProtocol
from litestar.plugins import CLIPlugin
from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
from litestar.testing import TestClient, create_test_client

Expand Down Expand Up @@ -374,7 +374,7 @@ def fn() -> None:


def test_plugin_properties() -> None:
class FooPlugin(CLIPluginProtocol):
class FooPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
return

Expand All @@ -386,7 +386,7 @@ def on_cli_init(self, cli: Group) -> None:


def test_plugin_registry() -> None:
class FooPlugin(CLIPluginProtocol):
class FooPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
return

Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_cli/test_cli_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ def test_basic_command(runner: CliRunner, create_app_file: CreateAppFileFixture,
app_file_content = textwrap.dedent(
"""
from litestar import Litestar
from litestar.plugins import CLIPluginProtocol
from litestar.plugins import CLIPlugin
class CLIPlugin(CLIPluginProtocol):
class MyCLIPlugin(CLIPlugin):
def on_cli_init(self, cli):
@cli.command()
def foo(app: Litestar):
print(f"App is loaded: {app is not None}")
app = Litestar(plugins=[CLIPlugin()])
app = Litestar(plugins=[MyCLIPlugin()])
"""
)
app_file = create_app_file("command_test_app.py", content=app_file_content)
Expand Down
24 changes: 12 additions & 12 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, InitPlugin, OpenAPISchemaPlugin, PluginRegistry
from litestar.plugins import CLIPlugin, 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 Down Expand Up @@ -45,11 +45,11 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig:


def test_plugin_registry() -> None:
class CLIPlugin(CLIPluginProtocol):
class MyCLIPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
pass

cli_plugin = CLIPlugin()
cli_plugin = MyCLIPlugin()
serialization_plugin = SQLAlchemySerializationPlugin()
openapi_plugin = PydanticSchemaPlugin()
init_plugin = PydanticInitPlugin()
Expand All @@ -70,32 +70,32 @@ def on_cli_init(self, cli: Group) -> None:


def test_plugin_registry_get() -> None:
class CLIPlugin(CLIPluginProtocol):
class MyCLIPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
pass

cli_plugin = CLIPlugin()
cli_plugin = MyCLIPlugin()

with pytest.raises(KeyError, match="No plugin of type 'CLIPlugin' registered"):
PluginRegistry([]).get(CLIPlugin)
with pytest.raises(KeyError, match="No plugin of type 'MyCLIPlugin' registered"):
PluginRegistry([]).get(MyCLIPlugin)

assert PluginRegistry([cli_plugin]).get(CLIPlugin) is cli_plugin
assert PluginRegistry([cli_plugin]).get(MyCLIPlugin) is cli_plugin


def test_plugin_registry_stringified_get() -> None:
class CLIPlugin(CLIPluginProtocol):
class MyCLIPlugin(CLIPlugin):
def on_cli_init(self, cli: Group) -> None:
pass

cli_plugin = CLIPlugin()
cli_plugin = MyCLIPlugin()
pydantic_plugin = PydanticPlugin()
with pytest.raises(KeyError):
PluginRegistry([CLIPlugin()]).get(
PluginRegistry([MyCLIPlugin()]).get(
"litestar2.plugins.pydantic.PydanticPlugin"
) # not a fqdn. should fail # type: ignore[list-item]
PluginRegistry([]).get("CLIPlugin") # not a fqdn. should fail # type: ignore[list-item]

assert PluginRegistry([cli_plugin, pydantic_plugin]).get(CLIPlugin) is cli_plugin
assert PluginRegistry([cli_plugin, pydantic_plugin]).get(MyCLIPlugin) is cli_plugin
assert PluginRegistry([cli_plugin, pydantic_plugin]).get(PydanticPlugin) is pydantic_plugin
assert PluginRegistry([cli_plugin, pydantic_plugin]).get("PydanticPlugin") is pydantic_plugin
assert (
Expand Down
Loading

0 comments on commit f635625

Please sign in to comment.