Skip to content

Commit

Permalink
Changes:
Browse files Browse the repository at this point in the history
- per stack encoders via context vars
- use the new encoders API features of lilya 0.11.0
- fix incompatibilities with lilya 0.11.0
  • Loading branch information
devkral committed Nov 26, 2024
1 parent 0e1e695 commit 5278988
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 130 deletions.
16 changes: 13 additions & 3 deletions docs/en/docs/encoders.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ from esmerald.encoders import Encoder

When subclassing the `Encoder`, the mandatory functions are:

**Serializing**
* [`is_type()`](#is_type)
* [`serialize()`](#serialize)


**Molding**

* [`is_type()`](#is_type)
* [`is_type_structure()`](#is_type_structure)
* [`encode()`](#encode)

Esmerald extends the native functionality of Lilya regarding the encoders and adds some extra flavours to it.
Expand All @@ -43,9 +50,6 @@ unique to Esmerald.
This function might sound confusing but it is in fact something simple. This function is used to check
if the object of type X is an instance or a subclass of that same type.

!!! Danger
Here is where it is different from Lilya. With Lilya you can use the `__type__` as well but
**not in Esmerald. In Esmerald you must implement the `is_type` function.

#### Example

Expand All @@ -71,6 +75,11 @@ Quite simple and intuitive.
{!> ../../../docs_src/encoders/serialize.py !}
```

### is_type_structure

For checking if an annotation can be used to mold an instance from the value.
In the second step it is verified via `is_type` if the molding is required of the type or the type does match already.

### encode

Finally, this functionality is what converts a given piece of data (JSON usually) into an object
Expand All @@ -84,6 +93,7 @@ For example, a dictionary into Pydantic models or MsgSpec Structs.
{!> ../../../docs_src/encoders/encode.py !}
```


### The flexibility

As you can see, there are many ways of you building your encoders. Esmerald internally already brings
Expand Down
14 changes: 14 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ hide:

# Release Notes

## Unreleased

### Added

- `ENCODER_TYPES_CTX` for using/setting the current encoders.
- Tests can be executed without changing the global ENCODER_TYPES state.

### Changed

- Leverage new lilya encoder API. Encoders can use `__type__`
- **Breaking** For custom Encoders implementing encode either `__type__` or `is_type_structure` must be provided.
- **Breaking** For Python >=3.10 eval_str is used for resolving the annotations of a function.
Make sure objects specified are not hidden with `TYPE_CHECKING` flag.

## 3.5.0

### Added
Expand Down
3 changes: 2 additions & 1 deletion docs_src/encoders/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@


class AttrsEncoder(Encoder):

def is_type(self, value: Any) -> bool:
return has(value)

is_type_structure = is_type

def serialize(self, obj: Any) -> Any:
return asdict(obj)

Expand Down
11 changes: 6 additions & 5 deletions docs_src/encoders/encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@


class MsgSpecEncoder(Encoder):

def is_type(self, value: Any) -> bool:
return isinstance(value, Struct) or is_class_and_subclass(value, Struct)
return isinstance(value, Struct)

def is_type_structure(self, value: Any) -> bool:
return is_class_and_subclass(value, Struct)

def serialize(self, obj: Any) -> Any:
return msgspec.json.decode(msgspec.json.encode(obj))
Expand All @@ -23,9 +25,8 @@ def encode(self, annotation: Any, value: Any) -> Any:


class PydanticEncoder(Encoder):

def is_type(self, value: Any) -> bool:
return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel)
# leverage the comfort lilya is_type and is_type_structure defaults
__type__ = BaseModel

def serialize(self, obj: BaseModel) -> dict[str, Any]:
return obj.model_dump()
Expand Down
43 changes: 32 additions & 11 deletions esmerald/applications.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import warnings
from collections.abc import Callable, Iterable, Sequence
from datetime import timezone as dtimezone
from functools import cached_property
from inspect import isclass
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Sequence,
Type,
TypeVar,
Union,
Expand All @@ -29,7 +29,11 @@
from esmerald.config.static_files import StaticFilesConfig
from esmerald.contrib.schedulers.base import SchedulerConfig
from esmerald.datastructures import State
from esmerald.encoders import Encoder, MsgSpecEncoder, PydanticEncoder, register_esmerald_encoder
from esmerald.encoders import (
EncoderProtocol,
MoldingProtocol,
register_esmerald_encoder,
)
from esmerald.exception_handlers import (
improperly_configured_exception_handler,
pydantic_validation_error_handler,
Expand Down Expand Up @@ -1024,7 +1028,16 @@ async def another(request: Request) -> str:
),
] = None,
encoders: Annotated[
Sequence[Optional[Encoder]],
Optional[
Sequence[
Union[
EncoderProtocol,
MoldingProtocol,
type[EncoderProtocol],
type[MoldingProtocol],
]
]
],
Doc(
"""
A `list` of encoders to be used by the application once it
Expand Down Expand Up @@ -1608,7 +1621,15 @@ def extend(self, config: PluggableConfig) -> None:
] = State()
self.async_exit_config = esmerald_settings.async_exit_config

self.encoders = self.load_settings_value("encoders", encoders) or []
self.encoders: list[Union[EncoderProtocol, MoldingProtocol]] = list(
cast(
Iterable[Union[EncoderProtocol, MoldingProtocol]],
(
encoder if isclass(encoder) else encoder
for encoder in self.load_settings_value("encoders", encoders) or []
),
)
)
self._register_application_encoders()

if self.enable_scheduler:
Expand Down Expand Up @@ -1662,9 +1683,6 @@ def _register_application_encoders(self) -> None:
This way, the support still remains but using the Lilya Encoders.
"""
self.register_encoder(cast(Encoder[Any], PydanticEncoder))
self.register_encoder(cast(Encoder[Any], MsgSpecEncoder))

for encoder in self.encoders:
self.register_encoder(encoder)

Expand Down Expand Up @@ -2577,13 +2595,16 @@ def default_settings(
"""
return esmerald_settings

async def globalise_settings(self) -> None:
async def globalize_settings(self) -> None:
"""
Making sure the global settings remain as is
after the request is done.
"""
esmerald_settings.configure(__lazy_settings__._wrapped)

# typo
globalise_settings = globalize_settings

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "lifespan":
await self.router.lifespan(scope, receive, send)
Expand All @@ -2594,7 +2615,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:

scope["state"] = {}
await super().__call__(scope, receive, send)
await self.globalise_settings()
await self.globalize_settings()

def websocket_route(self, path: str, name: Optional[str] = None) -> Callable:
raise ImproperlyConfigured("`websocket_route` is not valid. Use WebSocketGateway instead.")
Expand All @@ -2611,7 +2632,7 @@ def on_event(self, event_type: str) -> Callable: # pragma: nocover
def add_event_handler(self, event_type: str, func: Callable) -> None: # pragma: no cover
self.router.add_event_handler(event_type, func)

def register_encoder(self, encoder: Encoder[Any]) -> None:
def register_encoder(self, encoder: Union[EncoderProtocol, MoldingProtocol]) -> None:
"""
Registers a Encoder into the list of predefined encoders of the system.
"""
Expand Down
2 changes: 2 additions & 0 deletions esmerald/conf/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,8 @@ class AttrsEncoder(Encoder):
def is_type(self, value: Any) -> bool:
return has(value)
is_type_structure = is_type
def serialize(self, obj: Any) -> Any:
return asdict(obj)
Expand Down
110 changes: 39 additions & 71 deletions esmerald/encoders.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,70 @@
from __future__ import annotations

from typing import Any, TypeVar, get_args
from typing import Any, get_args

import msgspec
from lilya._internal._encoders import json_encoder as json_encoder # noqa
from lilya._utils import is_class_and_subclass
from lilya._internal._encoders import ModelDumpEncoder
from lilya.encoders import (
ENCODER_TYPES as LILYA_ENCODER_TYPES, # noqa
Encoder as LilyaEncoder, # noqa
register_encoder as register_encoder, # noqa
ENCODER_TYPES as ENCODER_TYPES_CTX, # noqa
Encoder,
EncoderProtocol, # noqa
MoldingProtocol, # noqa
json_encode,
register_encoder, # noqa
)
from msgspec import Struct
from pydantic import BaseModel
from pydantic_core import PydanticSerializationError

from esmerald.exceptions import ImproperlyConfigured
from esmerald.utils.helpers import is_union

T = TypeVar("T")
ENCODER_TYPES = ENCODER_TYPES_CTX.get()

ENCODER_TYPES = LILYA_ENCODER_TYPES.get()


class Encoder(LilyaEncoder[T]):
def is_type(self, value: Any) -> bool:
"""
Function that checks if the function is
an instance of a given type
"""
raise NotImplementedError("All Esmerald encoders must implement is_type() method.")

def serialize(self, obj: Any) -> Any:
"""
Function that transforms a data structure into a serializable
object.
"""
raise NotImplementedError("All Esmerald encoders must implement serialize() method.")

def encode(self, annotation: Any, value: Any) -> Any:
"""
Function that transforms the kwargs into a structure
"""
raise NotImplementedError("All Esmerald encoders must implement encode() method.")


class MsgSpecEncoder(Encoder):
def is_type(self, value: Any) -> bool:
return isinstance(value, Struct) or is_class_and_subclass(value, Struct)

def serialize(self, obj: Any) -> Any:
"""
When a `msgspec.Struct` is serialised,
it will call this function.
"""
return msgspec.json.decode(msgspec.json.encode(obj))

def encode(self, annotation: Any, value: Any) -> Any:
return msgspec.json.decode(msgspec.json.encode(value), type=annotation)
def register_esmerald_encoder(
encoder: EncoderProtocol | MoldingProtocol | type[EncoderProtocol] | type[MoldingProtocol],
) -> None:
"""
Registers an esmerald encoder into available Lilya encoders
"""
try:
register_encoder(encoder)
except RuntimeError:
raise ImproperlyConfigured(f"{type(encoder)} must be a subclass of Encoder") from None


class PydanticEncoder(Encoder):
def is_type(self, value: Any) -> bool:
return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel)
PydanticEncoder = ModelDumpEncoder
try:
import msgspec

def serialize(self, obj: BaseModel) -> dict[str, Any]:
try:
return obj.model_dump(mode="json")
except PydanticSerializationError:
return obj.model_dump()
class MsgSpecEncoder(Encoder):
__type__ = msgspec.Struct

def encode(self, annotation: Any, value: Any) -> Any:
if isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel):
return value
return annotation(**value)
def serialize(self, obj: Any) -> Any:
"""
When a `msgspec.Struct` is serialised,
it will call this function.
"""
return msgspec.json.decode(msgspec.json.encode(obj))

def encode(self, annotation: Any, value: Any) -> Any:
return msgspec.json.decode(msgspec.json.encode(value), type=annotation)

def register_esmerald_encoder(encoder: Encoder[Any]) -> None:
"""
Registers an esmerald encoder into available Lilya encoders
"""
if not isinstance(encoder, Encoder) and not is_class_and_subclass(encoder, Encoder): # type: ignore
raise ImproperlyConfigured(f"{type(encoder)} must be a subclass of Encoder")
register_esmerald_encoder(MsgSpecEncoder)
except ImportError:
pass

encoder_types = {encoder.__class__.__name__ for encoder in ENCODER_TYPES}
if encoder.__name__ not in encoder_types:
register_encoder(encoder)
json_encoder = json_encode


def is_body_encoder(value: Any) -> bool:
"""
Function that checks if the value is a body encoder.
"""
encoder_types = ENCODER_TYPES_CTX.get()
if not is_union(value):
return any(encoder.is_type(value) for encoder in ENCODER_TYPES)
return any(encoder.is_type(value) for encoder in encoder_types)

union_arguments = get_args(value)
if not union_arguments:
return False
return any(
any(encoder.is_type(argument) for encoder in ENCODER_TYPES) for argument in union_arguments
any(encoder.is_type(argument) for encoder in encoder_types) for argument in union_arguments
)
Loading

0 comments on commit 5278988

Please sign in to comment.