Skip to content

Commit

Permalink
✨ Single type injection (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
perdy authored and migduroli committed Sep 3, 2024
1 parent cd06bdb commit a2ef280
Show file tree
Hide file tree
Showing 23 changed files with 693 additions and 474 deletions.
2 changes: 1 addition & 1 deletion flama/ddd/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self, worker: AbstractWorker):
self.worker = worker

def can_handle_parameter(self, parameter: "Parameter") -> bool:
return parameter.type is self.worker.__class__
return parameter.annotation is self.worker.__class__

def resolve(self, scope: types.Scope):
self.worker.app = scope["root_app"]
Expand Down
8 changes: 4 additions & 4 deletions flama/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def handler(self) -> t.Callable:
async def dispatch(self) -> None:
"""Dispatch a request."""
app = self.state["app"]
handler = await app.injector.inject(self.handler, **self.state)
handler = await app.injector.inject(self.handler, self.state)
return await concurrency.run(handler)


Expand Down Expand Up @@ -123,14 +123,14 @@ async def dispatch(self) -> None:
app = self.state["app"]
websocket = self.state["websocket"]

on_connect = await app.injector.inject(self.on_connect, **self.state)
on_connect = await app.injector.inject(self.on_connect, self.state)
await on_connect()

try:
self.state["websocket_message"] = await websocket.receive()

while websocket.is_connected:
on_receive = await app.injector.inject(self.on_receive, **self.state)
on_receive = await app.injector.inject(self.on_receive, self.state)
await on_receive()
self.state["websocket_message"] = await websocket.receive()

Expand All @@ -145,7 +145,7 @@ async def dispatch(self) -> None:
self.state["websocket_code"] = types.Code(1011)
raise e from None
finally:
on_disconnect = await app.injector.inject(self.on_disconnect, **self.state)
on_disconnect = await app.injector.inject(self.on_disconnect, self.state)
await on_disconnect()

async def on_connect(self, websocket: websockets.WebSocket) -> None:
Expand Down
12 changes: 11 additions & 1 deletion flama/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ def __init__(

def __str__(self) -> str:
params = ("path", "params", "name")
formatted_params = ", ".join([f"{x}={getattr(self, x)}" for x in params if getattr(self, x)])
formatted_params = ", ".join([f"{x}={repr(getattr(self, x))}" for x in params if getattr(self, x)])
return f"Path not found ({formatted_params})"

def __repr__(self) -> str:
params = ("path", "params", "name")
formatted_params = ", ".join([f"{x}={repr(getattr(self, x))}" for x in params if getattr(self, x)])
return f"{self.__class__.__name__}({formatted_params})"


Expand All @@ -120,6 +125,11 @@ def __init__(
self.allowed = allowed

def __str__(self) -> str:
params = ("path", "params", "method", "allowed")
formatted_params = ", ".join([f"{x}={getattr(self, x)}" for x in params if getattr(self, x)])
return f"Method not allowed ({formatted_params})"

def __repr__(self) -> str:
params = ("path", "params", "method", "allowed")
formatted_params = ", ".join([f"{x}={getattr(self, x)}" for x in params if getattr(self, x)])
return f"{self.__class__.__name__}({formatted_params})"
23 changes: 14 additions & 9 deletions flama/injection/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import inspect
import typing as t

from flama.injection.exceptions import ComponentNotFound
from flama.injection.exceptions import ComponentError, ComponentNotFound
from flama.injection.resolver import Parameter

__all__ = ["Component", "Components"]
Expand All @@ -18,10 +18,10 @@ def identity(self, parameter: Parameter) -> str:
:return: Unique identifier.
"""
try:
parameter_type = parameter.type.__name__
parameter_type = parameter.annotation.__name__
except AttributeError:
parameter_type = parameter.type.__class__.__name__
component_id = f"{id(parameter.type)}:{parameter_type}"
parameter_type = parameter.annotation.__class__.__name__
component_id = f"{id(parameter.annotation)}:{parameter_type}"

# If `resolve` includes `Parameter` then use an id that is additionally parameterized by the parameter name.
args = inspect.signature(self.resolve).parameters.values() # type: ignore[attr-defined]
Expand All @@ -41,12 +41,13 @@ def can_handle_parameter(self, parameter: Parameter) -> bool:
:return: True if this component can handle the given parameter.
"""
return_annotation = inspect.signature(self.resolve).return_annotation # type: ignore[attr-defined]
assert return_annotation is not inspect.Signature.empty, (
f"Component '{self.__class__.__name__}' must include a return annotation on the 'resolve' method, or "
f"override 'can_handle_parameter'"
)
if return_annotation is inspect.Signature.empty:
raise ComponentError(
f"Component '{self.__class__.__name__}' must include a return annotation on the 'resolve' method, or "
f"override 'can_handle_parameter'"
)

return parameter.type is return_annotation
return parameter.annotation is return_annotation

def signature(self) -> t.Dict[str, Parameter]:
"""Component resolver signature.
Expand All @@ -58,6 +59,10 @@ def signature(self) -> t.Dict[str, Parameter]:
for k, v in inspect.signature(self.resolve).parameters.items() # type: ignore[attr-defined]
}

@property
def use_parameter(self) -> bool:
return any((x for x in self.signature().values() if x.annotation is Parameter))

async def __call__(self, *args, **kwargs):
"""Performs a resolution by calling this component's resolve method.
Expand Down
8 changes: 6 additions & 2 deletions flama/injection/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
from flama.injection.components import Component
from flama.injection.resolver import Parameter

__all__ = ["ComponentNotFound"]
__all__ = ["ComponentError", "ComponentNotFound"]


class ComponentNotFound(Exception):
class ComponentError(Exception):
...


class ComponentNotFound(ComponentError):
def __init__(
self,
parameter: "Parameter",
Expand Down
72 changes: 61 additions & 11 deletions flama/injection/injector.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import functools
import inspect
import typing as t

from flama.injection.components import Component, Components
from flama.injection.resolver import Resolver
from flama.injection.exceptions import ComponentNotFound
from flama.injection.resolver import EMPTY, ROOT_NAME, Parameter, Resolver

if t.TYPE_CHECKING:
from flama.injection.resolver import ParametersTree
from flama.injection.resolver import ResolutionTree


class Injector:
"""Functions dependency injector. It uses a resolver to generate dependencies trees and evaluate them."""

def __init__(
self,
context_types: t.Optional[t.Dict[str, t.Type]] = None,
components: t.Optional[t.Union[t.Sequence[Component], Components]] = None,
):
"""Functions dependency injector.
It uses a resolver to generate dependencies trees and evaluate them.
:param context_types: A mapping of names into types for injection contexts.
:param components: List of components.
"""
Expand Down Expand Up @@ -62,21 +68,65 @@ def resolver(self) -> Resolver:
def resolver(self):
self._resolver = None

def resolve(self, func: t.Callable) -> "ParametersTree":
@t.overload
def resolve(self, annotation: t.Any):
...

@t.overload
def resolve(self, annotation: t.Any, *, name: str):
...

@t.overload
def resolve(self, annotation: t.Any, *, default: t.Any):
...

@t.overload
def resolve(self, annotation: t.Any, *, name: str, default: t.Any):
...

def resolve(
self, annotation: t.Optional[t.Any] = None, *, name: t.Optional[str] = None, default: t.Any = EMPTY
) -> "ResolutionTree":
"""Generate a dependencies tree for a given type annotation.
:param annotation: Type annotation to be resolved.
:param name: Name of the parameter to be resolved.
:param func: Function to be resolved.
:return: Dependencies tree.
"""
Inspects a function and creates a resolution list of all components needed to run it.
return self.resolver.resolve(Parameter(name or ROOT_NAME, annotation, default))

def resolve_function(self, func: t.Callable) -> t.Dict[str, "ResolutionTree"]:
"""Generate a dependencies tree for a given function.
It analyses the function signature, look for type annotations and try to resolve them.
:param func: Function to resolve.
:return: The parameters resolution tree.
:param func: Function to be resolved.
:return: Mapping of parameter names and dependencies trees.
"""
return self.resolver.resolve(func)
parameters = {}
for p in [x for x in inspect.signature(func).parameters.values() if x.name not in ("self", "cls")]:
try:
parameters[p.name] = self.resolver.resolve(Parameter.from_parameter(p))
except ComponentNotFound as e:
raise ComponentNotFound(e.parameter, component=e.component, function=func) from None

async def inject(self, func: t.Callable, **context: t.Any) -> t.Callable:
"""Given a function, injects all components and types defined in its signature and returns the partialised
function.
return parameters

async def inject(self, func: t.Callable, context: t.Optional[t.Dict[str, t.Any]] = None) -> t.Callable:
"""Inject dependencies into a given function.
It analyses the function signature, look for type annotations and try to resolve them. Once all dependencies
trees are resolved for every single parameter, it uses the given context to evaluate those trees and calculate
a final value for each parameter. Finally, it returns a partialised function with all dependencies injected.
:param func: Function to be partialised.
:param context: Mapping of names and values used to gather injection values.
:return: Partialised function with all dependencies injected.
"""
return functools.partial(func, **(await self.resolve(func).context(**context)))
if context is None:
context = {}

return functools.partial(
func, **{name: await resolution.value(context) for name, resolution in self.resolve_function(func).items()}
)
Loading

0 comments on commit a2ef280

Please sign in to comment.