Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggable/Extensions: allow string imports instead of classes #456

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/en/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ starting the system.

It is this simple but is it the only way to add a pluggable into the system? **Short answser is no**.

More details about this in [hooking a pluggable into the application](#hooking-pluggables).
More details about this in [hooking a pluggable into the application](#hooking-pluggables-and-extensions).

## Extension

Expand Down Expand Up @@ -174,7 +174,7 @@ And simply start the application.

```shell
ESMERALD_SETTINGS_MODULE=AppSettings uvicorn src:app --reload

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
Expand All @@ -186,7 +186,7 @@ And simply start the application.

```shell
$env:ESMERALD_SETTINGS_MODULE="AppSettings"; uvicorn src:app --reload

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
Expand Down
6 changes: 6 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ hide:

# Release Notes

## 3.6.1

### Added

- Allow passing extensions as string.

## 3.6.0

### Added
Expand Down
6 changes: 5 additions & 1 deletion docs_src/pluggables/pluggable.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ def extend(self, config: PluggableConfig) -> None:

pluggable = Pluggable(MyExtension, config=my_config)

app = Esmerald(routes=[], extensions={"my-extension": pluggable})
# it is also possible to just pass strings instead of pluggables but this way you lose the ability to pass arguments
app = Esmerald(
routes=[],
extensions={"my-extension": pluggable, "my-other-extension": Pluggable("path.to.extension")},
)
4 changes: 2 additions & 2 deletions esmerald/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,7 @@ async def home() -> Dict[str, str]:
),
] = None,
extensions: Annotated[
Optional[Dict[str, Union[Extension, Pluggable, type[Extension]]]],
Optional[Dict[str, Union[Extension, Pluggable, type[Extension], str]]],
Doc(
"""
A `list` of global extensions from objects inheriting from
Expand Down Expand Up @@ -1395,7 +1395,7 @@ def extend(self, config: PluggableConfig) -> None:
),
] = None,
pluggables: Annotated[
Optional[Dict[str, Union[Extension, Pluggable, type[Extension]]]],
Optional[Dict[str, Union[Extension, Pluggable, type[Extension], str]]],
Doc(
"""
THIS PARAMETER IS DEPRECATED USE extensions INSTEAD
Expand Down
24 changes: 19 additions & 5 deletions esmerald/pluggables/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from inspect import isclass
from typing import TYPE_CHECKING, Any, Iterator, Optional
from typing import TYPE_CHECKING, Any, Iterator, Optional, cast

from lilya._internal._module_loading import import_string
from typing_extensions import Annotated, Doc

from esmerald.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -49,15 +52,24 @@ def extend(self, config: PluggableConfig) -> None:
my_config = PluggableConfig(name="my extension")

pluggable = Pluggable(MyExtension, config=my_config)
# or
# pluggable = Pluggable("path.to.MyExtension", config=my_config)

app = Esmerald(routes=[], extensions={"my-extension": pluggable})
```
"""

def __init__(self, cls: type["ExtensionProtocol"], **options: Any):
self.cls = cls
def __init__(self, cls: type[ExtensionProtocol] | str, **options: Any):
self.cls_or_string = cls
self.options = options

@property
def cls(self) -> type[ExtensionProtocol]:
cls_or_string = self.cls_or_string
if isinstance(cls_or_string, str):
self.cls_or_string = cls_or_string = import_string(cls_or_string)
return cast(type["ExtensionProtocol"], cls_or_string)

def __iter__(self) -> Iterator:
iterator = (self.cls, self.options)
return iter(iterator)
Expand Down Expand Up @@ -104,7 +116,7 @@ def extend(self, **kwargs: "DictAny") -> None:
def __init__(
self,
app: Annotated[
Optional["Esmerald"],
Optional[Esmerald],
Doc(
"""
An `Esmerald` application instance or subclasses of Esmerald.
Expand All @@ -128,7 +140,7 @@ def extend(self, **kwargs: Any) -> None:


class ExtensionDict(dict[str, Extension]):
def __init__(self, data: Any = None, *, app: "Esmerald"):
def __init__(self, data: Any = None, *, app: Esmerald):
super().__init__(data)
self.app = app
self.delayed_extend: Optional[dict[str, dict[str, Any]]] = {}
Expand Down Expand Up @@ -167,6 +179,8 @@ def extend(self, **kwargs: "DictAny") -> None:
self[name].extend(**val)

def __setitem__(self, name: Any, value: Any) -> None:
if isinstance(value, str):
value = Pluggable(value)
if not isinstance(name, str):
raise ImproperlyConfigured("Extension names should be in string format.")
elif isinstance(value, Pluggable):
Expand Down
8 changes: 6 additions & 2 deletions esmerald/testclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ def create_client(
backend: "Literal['asyncio', 'trio']" = "asyncio",
backend_options: Optional[Dict[str, Any]] = None,
interceptors: Optional[List["Interceptor"]] = None,
pluggables: Optional[Dict[str, Union["Extension", "Pluggable", type["Extension"]]]] = None,
extensions: Optional[Dict[str, Union["Extension", "Pluggable", type["Extension"]]]] = None,
pluggables: Optional[
Dict[str, Union["Extension", "Pluggable", type["Extension"], str]]
] = None,
extensions: Optional[
Dict[str, Union["Extension", "Pluggable", type["Extension"], str]]
] = None,
permissions: Optional[List["Permission"]] = None,
dependencies: Optional["Dependencies"] = None,
middleware: Optional[List["Middleware"]] = None,
Expand Down
8 changes: 8 additions & 0 deletions tests/pluggables/import_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from loguru import logger

from esmerald import Extension


class MyExtension2(Extension):
def extend(self) -> None:
logger.info("Started extension2")
12 changes: 11 additions & 1 deletion tests/pluggables/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,20 @@ def extend(self, config: Config) -> None:

def test_generates_pluggable():
app = Esmerald(
routes=[], extensions={"test": Pluggable(MyExtension, config=Config(name="my pluggable"))}
routes=[],
extensions={
"test": Pluggable(MyExtension, config=Config(name="my pluggable")),
"test2": Pluggable("tests.pluggables.import_target.MyExtension2"),
"test3": "tests.pluggables.import_target.MyExtension2",
},
)

assert "test" in app.extensions
assert isinstance(app.extensions["test"], Extension)
assert "test2" in app.extensions
assert isinstance(app.extensions["test2"], Extension)
assert "test3" in app.extensions
assert isinstance(app.extensions["test3"], Extension)


def test_generates_many_pluggables():
Expand Down
Loading