From 722a91311fc36e0740e24fa585a81bb125018dc4 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 18 Dec 2024 12:14:09 +0100 Subject: [PATCH] allow import strings for Pluggables Changes: - allow import strings for Pluggable - allow passing just import strings for extensions --- docs/en/docs/extensions.md | 6 +++--- docs_src/pluggables/pluggable.py | 6 +++++- esmerald/applications.py | 4 ++-- esmerald/pluggables/base.py | 18 +++++++++++++++--- esmerald/testclient.py | 8 ++++++-- tests/pluggables/import_target.py | 8 ++++++++ tests/pluggables/test_extensions.py | 12 +++++++++++- 7 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 tests/pluggables/import_target.py diff --git a/docs/en/docs/extensions.md b/docs/en/docs/extensions.md index 56d59464..7634878a 100644 --- a/docs/en/docs/extensions.md +++ b/docs/en/docs/extensions.md @@ -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 @@ -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] @@ -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] diff --git a/docs_src/pluggables/pluggable.py b/docs_src/pluggables/pluggable.py index 3070e357..0eeb5ba1 100644 --- a/docs_src/pluggables/pluggable.py +++ b/docs_src/pluggables/pluggable.py @@ -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")}, +) diff --git a/esmerald/applications.py b/esmerald/applications.py index eee2e11e..dcff8876 100644 --- a/esmerald/applications.py +++ b/esmerald/applications.py @@ -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 @@ -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 diff --git a/esmerald/pluggables/base.py b/esmerald/pluggables/base.py index 00e2c96c..c40b61a5 100644 --- a/esmerald/pluggables/base.py +++ b/esmerald/pluggables/base.py @@ -1,7 +1,8 @@ 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 @@ -49,15 +50,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) @@ -167,6 +177,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): diff --git a/esmerald/testclient.py b/esmerald/testclient.py index 0f6295e8..14992ad7 100644 --- a/esmerald/testclient.py +++ b/esmerald/testclient.py @@ -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, diff --git a/tests/pluggables/import_target.py b/tests/pluggables/import_target.py new file mode 100644 index 00000000..11e8a9a5 --- /dev/null +++ b/tests/pluggables/import_target.py @@ -0,0 +1,8 @@ +from loguru import logger + +from esmerald import Extension + + +class MyExtension2(Extension): + def extend(self) -> None: + logger.info("Started extension2") diff --git a/tests/pluggables/test_extensions.py b/tests/pluggables/test_extensions.py index 7f873552..ece8eb65 100644 --- a/tests/pluggables/test_extensions.py +++ b/tests/pluggables/test_extensions.py @@ -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():