Skip to content

Commit

Permalink
Changes:
Browse files Browse the repository at this point in the history
- fix pluggables logic, pluggables contains now extensions and convert
  pluggables on the fly
- unrestrict pluggables, assigning extensions instances and classes is
  now possible too. The extend logic is now executed automatically
  • Loading branch information
devkral committed Nov 8, 2024
1 parent 51cb1d2 commit f03e157
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 85 deletions.
10 changes: 9 additions & 1 deletion docs/en/docs/pluggables.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,18 @@ This way you don't need to use the [Pluggable](#pluggable) object in any way and
simply just use the [Extension](#extension) class or even your own since you **are in control**
of the extension.

```python hl_lines="25 42-43"
There are two variants how to do it:

```python title="With extension class or Pluggable"
{!> ../../../docs_src/pluggables/manual.py !}
```

```python hl_lines="25 42-43" title="Self registering"
{!> ../../../docs_src/pluggables/manual_self_registering.py !}
```

You can use for the late registration the methods `add_extension` or `add_pluggable`. `add_pluggable` is an alias.

### Standalone object

But, what if I don't want to use the [Extension](#extension) object for my pluggable? Is this
Expand Down
5 changes: 5 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ hide:

- Allow passing HTTP/WebSocket handlers directly to routes. They are automatically wrapped in Gateways-

### Changed

- Pluggables can now receive plain Extensions and Extension classes.
- Alias add_pluggable with add_extension.

## 3.4.4

### Added
Expand Down
10 changes: 3 additions & 7 deletions docs_src/pluggables/manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ def extend(self, **kwargs: "DictAny") -> None:
# Do something here like print a log or whatever you need
logger.success("Started the extension manually")

# Add the extension to the pluggables of Esmerald
# And make it accessible
self.app.add_pluggable("my-extension", self)


@get("/home")
async def home(request: Request) -> JSONResponse:
Expand All @@ -38,6 +34,6 @@ async def home(request: Request) -> JSONResponse:


app = Esmerald(routes=[Gateway(handler=home)])

extension = MyExtension(app=app)
extension.extend()
app.add_pluggable("my-extension", MyExtension)
# or
# app.add_extension("my-extension", MyExtension)
43 changes: 43 additions & 0 deletions docs_src/pluggables/manual_self_registering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Optional

from loguru import logger

from esmerald import Esmerald, Extension, Gateway, JSONResponse, Request, get
from esmerald.types import DictAny


class MyExtension(Extension):
def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
super().__init__(app, **kwargs)
self.app = app
self.kwargs = kwargs

def extend(self, **kwargs: "DictAny") -> None:
"""
Function that should always be implemented when extending
the Extension class or a `NotImplementedError` is raised.
"""
# Do something here like print a log or whatever you need
logger.success("Started the extension manually")

# Add the extension to the pluggables of Esmerald
# And make it accessible
self.app.add_pluggable("my-extension", self)


@get("/home")
async def home(request: Request) -> JSONResponse:
"""
Returns a list of pluggables of the system.
"pluggables": ["my-extension"]
"""
pluggables = list(request.app.pluggables)

return JSONResponse({"pluggables": pluggables})


app = Esmerald(routes=[Gateway(handler=home)])

extension = MyExtension(app=app)
extension.extend()
1 change: 0 additions & 1 deletion docs_src/pluggables/standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

class Standalone:
def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
super().__init__(app, **kwargs)
self.app = app
self.kwargs = kwargs

Expand Down
74 changes: 34 additions & 40 deletions esmerald/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from esmerald.openapi.schemas.v3_1_0 import Contact, License, SecurityScheme
from esmerald.openapi.schemas.v3_1_0.open_api import OpenAPI
from esmerald.permissions.types import Permission
from esmerald.pluggables import Extension, Pluggable
from esmerald.pluggables import Extension, ExtensionDict, Pluggable
from esmerald.protocols.template import TemplateEngineProtocol
from esmerald.routing import gateways
from esmerald.routing.apis import base
Expand Down Expand Up @@ -1342,7 +1342,7 @@ async def home() -> Dict[str, str]:
),
] = None,
pluggables: Annotated[
Optional[Dict[str, Pluggable]],
Optional[Dict[str, Union[Extension, Pluggable, type[Extension]]]],
Doc(
"""
A `list` of global pluggables from objects inheriting from
Expand Down Expand Up @@ -1528,7 +1528,6 @@ def extend(self, config: PluggableConfig) -> None:
self.redirect_slashes = self.load_settings_value(
"redirect_slashes", redirect_slashes, is_boolean=True
)
self.pluggables = self.load_settings_value("pluggables", pluggables)

# OpenAPI Related
self.root_path_in_servers = self.load_settings_value(
Expand Down Expand Up @@ -1578,9 +1577,12 @@ def extend(self, config: PluggableConfig) -> None:
self.get_default_exception_handlers()
self.user_middleware = self.build_user_middleware_stack()
self.middleware_stack = self.build_middleware_stack()
self.pluggable_stack = self.build_pluggable_stack()
self.template_engine = self.get_template_engine(self.template_config)

# load pluggables nearly last so everythings is initialized
self.pluggables = ExtensionDict(
self.load_settings_value("pluggables", pluggables), app=cast(Esmerald, self)
)
self._configure()

def _register_application_encoders(self) -> None:
Expand Down Expand Up @@ -1826,13 +1828,15 @@ def add_apiview(
```python
from esmerald import Esmerald, APIView, Gateway, get
class View(APIView):
path = "/"
@get(status_code=status_code)
async def hello(self) -> str:
return "Hello, World!"
gateway = Gateway(handler=View)
app = Esmerald()
Expand Down Expand Up @@ -1955,10 +1959,12 @@ def add_route(
```python
from esmerald import Esmerald, get
@get(status_code=status_code)
async def hello() -> str:
return "Hello, World!"
app = Esmerald()
app.add_route(path="/hello", handler=hello)
```
Expand Down Expand Up @@ -2067,6 +2073,7 @@ def add_websocket_route(
```python
from esmerald import Esmerald, websocket
@websocket()
async def websocket_route(socket: WebSocket) -> None:
await socket.accept()
Expand All @@ -2076,6 +2083,7 @@ async def websocket_route(socket: WebSocket) -> None:
await socket.send_json({"data": "esmerald"})
await socket.close()
app = Esmerald()
app.add_websocket_route(path="/ws", handler=websocket_route)
```
Expand Down Expand Up @@ -2113,10 +2121,12 @@ def add_include(
```python
from esmerald import get, Include
@get(status_code=status_code)
async def hello(self) -> str:
return "Hello, World!"
include = Include("/child", routes=[Gateway(handler=hello)])
app = Esmerald()
Expand Down Expand Up @@ -2212,10 +2222,12 @@ def add_router(
from esmerald import get
from esmerald.routing.router import Router
@get(status_code=status_code)
async def hello(self) -> str:
return "Hello, World!"
router = Router(path="/aditional", routes=[Gateway(handler=hello)])
app = Esmerald()
Expand Down Expand Up @@ -2415,37 +2427,9 @@ def build_middleware_stack(self) -> "ASGIApp":
app = cls(app=app, *args, **kwargs) # noqa
return app

def build_pluggable_stack(self) -> Optional["Esmerald"]:
"""
Validates the pluggable types passed and builds the stack
and triggers the plug
"""
if not self.pluggables:
return None

pluggables = {}

for name, extension in self.pluggables.items():
if not isinstance(name, str):
raise ImproperlyConfigured("Pluggable names should be in string format.")
elif isinstance(extension, Pluggable):
pluggables[name] = extension
continue
elif not is_class_and_subclass(extension, Extension):
raise ImproperlyConfigured(
"An extension must subclass from esmerald.pluggables.Extension and added to "
"a Pluggable object"
)

app: "ASGIApp" = self
for name, pluggable in pluggables.items():
for cls, options in [pluggable]:
ext: "Extension" = cls(app=app, **options)
ext.extend(**options)
self.pluggables[name] = cls
return cast("Esmerald", app)

def add_pluggable(self, name: str, extension: "Extension") -> None:
def add_extension(
self, name: str, extension: Union[Extension, Pluggable, type[Extension]]
) -> None:
"""
Adds a [Pluggable](https://esmerald.dev/pluggables/) directly to the active application router.
Expand All @@ -2457,27 +2441,37 @@ def add_pluggable(self, name: str, extension: "Extension") -> None:
from pydantic import BaseModel
class Config(BaseModel):
name: Optional[str]
class CustomExtension(Extension):
def __init__(self, app: Optional["Esmerald"] = None, **kwargs: DictAny):
super().__init__(app, **kwargs)
self.app = app
def extend(self, config) -> None:
logger.success(f"Started standalone plugging with the name: {config.name}")
logger.success(
f"Started standalone plugging with the name: {config.name}"
)
# you can also autoadd the extension like this
# self.app.add_pluggable(config.name, self)
self.app.add_pluggable("manual", self)
app = Esmerald(routes=[])
config = Config(name="manual")
extension = CustomExtension(app=app)
extension.extend(config=config)
pluggable = Pluggable(CustomExtension, config=config)
app.add_extension("manual", pluggable)
# or
# app.add_pluggable("manual", pluggable)
```
"""
self.pluggables[name] = extension

add_pluggable = add_extension

@property
def settings(self) -> Type["EsmeraldAPISettings"]:
"""
Expand Down Expand Up @@ -3470,7 +3464,7 @@ class ChildEsmerald(Esmerald):
```python
from esmerald import Esmerald, ChildEsmerald, Include
app = Esmerald(routes=[Include('/child', app=ChildEsmerald(...))])
app = Esmerald(routes=[Include("/child", app=ChildEsmerald(...))])
```
"""

Expand Down
4 changes: 2 additions & 2 deletions esmerald/pluggables/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .base import Extension, Pluggable
from .base import Extension, ExtensionDict, Pluggable

__all__ = ["Pluggable", "Extension"]
__all__ = ["Pluggable", "Extension", "ExtensionDict"]
Loading

0 comments on commit f03e157

Please sign in to comment.