Skip to content
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
20 changes: 20 additions & 0 deletions documentation/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,23 @@ app = Application(
services=InjectionServices(custom_module),
)
```

## [Typer](https://github.com/tiangolo/typer)

Example:

```python
from typing import Annotated

from injection import inject
from injection.integrations.typer import ignore
from typer import Typer

app = Typer()


@app.command()
@inject(force=True)
def my_command(dependency: Annotated[Dependency, ignore()]):
""" command implementation """
```
25 changes: 22 additions & 3 deletions injection/_pkg.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,21 @@ class Module:

def __init__(self, name: str = ...): ...
def __contains__(self, cls: type | UnionType, /) -> bool: ...
def inject(self, wrapped: Callable[..., Any] = ..., /):
def inject(
self,
wrapped: Callable[..., Any] = ...,
/,
*,
force: bool = ...,
):
"""
Decorator applicable to a class or function. Inject function dependencies using
parameter type annotations. If applied to a class, the dependencies resolved
will be those of the `__init__` method.

With `force=True`, parameters passed to replace dependencies will be ignored.
"""

def injectable(
self,
wrapped: Callable[..., Any] = ...,
Expand All @@ -57,6 +66,7 @@ class Module:
injectable will be constructed. At injection time, a new instance will be
injected each time.
"""

def singleton(
self,
wrapped: Callable[..., Any] = ...,
Expand All @@ -69,6 +79,7 @@ class Module:
singleton will be constructed. At injection time, the injected instance will
always be the same.
"""

def set_constant(
self,
instance: _T,
Expand All @@ -79,11 +90,14 @@ class Module:
registering global variables. The difference with the singleton decorator is
that no dependencies are resolved, so the module doesn't need to be locked.
"""
def get_instance(self, cls: type[_T]) -> _T | None:

def get_instance(self, cls: type[_T], none: bool = ...) -> _T | None:
"""
Function used to retrieve an instance associated with the type passed in
parameter or return `None`.
parameter or return `None` but if `none` parameter is `False` an exception
will be raised.
"""

def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
"""
Function used to retrieve an instance associated with the type passed in
Expand All @@ -92,16 +106,19 @@ class Module:

Example: instance = ~lazy_instance
"""

def use(self, module: Module, priority: ModulePriorities = ...):
"""
Function for using another module. Using another module replaces the module's
dependencies with those of the module used. If the dependency is not found, it
will be searched for in the module's dependency container.
"""

def stop_using(self, module: Module):
"""
Function to remove a module in use.
"""

def use_temporarily(
self,
module: Module,
Expand All @@ -110,6 +127,7 @@ class Module:
"""
Context manager or decorator for temporary use of a module.
"""

def change_priority(self, module: Module, priority: ModulePriorities):
"""
Function for changing the priority of a module in use.
Expand All @@ -118,6 +136,7 @@ class Module:
* **LOW**: The module concerned becomes the least important of the modules used.
* **HIGH**: The module concerned becomes the most important of the modules used.
"""

def unlock(self):
"""
Function to unlock the module by deleting cached instances of singletons.
Expand Down
45 changes: 30 additions & 15 deletions injection/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,13 @@ def __str__(self) -> str:
class Injectable(Protocol[_T]):
__slots__ = ()

def __init__(self, factory: Callable[[], _T] = ..., *args, **kwargs):
...
def __init__(self, __factory: Callable[[], _T] = ..., /): ...

@property
def is_locked(self) -> bool:
return False

def unlock(self):
...
def unlock(self): ...

@abstractmethod
def get_instance(self) -> _T:
Expand Down Expand Up @@ -332,29 +330,33 @@ def inject(
wrapped: Callable[..., Any] = None,
/,
*,
force: bool = False,
return_factory: bool = False,
):
def decorator(wp):
if not return_factory and isclass(wp):
wp.__init__ = decorator(wp.__init__)
wp.__init__ = self.inject(wp.__init__, force=force)
return wp

lazy_binder = Lazy[Binder](lambda: self.__new_binder(wp))

@wraps(wp)
def wrapper(*args, **kwargs):
arguments = (~lazy_binder).bind(*args, **kwargs)
arguments = (~lazy_binder).bind(args, kwargs, force)
return wp(*arguments.args, **arguments.kwargs)

return wrapper

return decorator(wrapped) if wrapped else decorator

def get_instance(self, cls: type[_T]) -> _T | None:
def get_instance(self, cls: type[_T], none: bool = True) -> _T | None:
try:
injectable = self[cls]
except KeyError:
return None
except KeyError as exc:
if none:
return None

raise exc from exc

instance = injectable.get_instance()
return cast(cls, instance)
Expand Down Expand Up @@ -476,8 +478,8 @@ def __iter__(self) -> Iterator[tuple[str, Any]]:
yield name, injectable.get_instance()

@property
def arguments(self) -> dict[str, Any]:
return dict(self)
def arguments(self) -> OrderedDict[str, Any]:
return OrderedDict(self)

@classmethod
def from_mapping(cls, mapping: Mapping[str, Injectable]):
Expand Down Expand Up @@ -519,21 +521,34 @@ def __init__(self, signature: Signature):
self.__signature = signature
self.__dependencies = Dependencies.empty()

def bind(self, /, *args, **kwargs) -> Arguments:
def bind(
self,
args: Iterable[Any] = (),
kwargs: Mapping[str, Any] = None,
force: bool = False,
) -> Arguments:
if kwargs is None:
kwargs = {}

if not self.__dependencies:
return Arguments(args, kwargs)

bound = self.__signature.bind_partial(*args, **kwargs)
bound.arguments = self.__dependencies.arguments | bound.arguments
dependencies = self.__dependencies.arguments

if force:
bound.arguments |= dependencies
else:
bound.arguments = dependencies | bound.arguments

return Arguments(bound.args, bound.kwargs)

def update(self, module: Module):
self.__dependencies = Dependencies.resolve(self.__signature, module)
return self

@singledispatchmethod
def on_event(self, event: Event, /):
...
def on_event(self, event: Event, /): ...

@on_event.register
@contextmanager
Expand Down
10 changes: 3 additions & 7 deletions injection/integrations/blacksheep.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Any, TypeVar

from injection import Module, default_module
from injection.exceptions import NoInjectable

__all__ = ("InjectionServices",)

_T = TypeVar("_T")

Expand All @@ -24,9 +25,4 @@ def register(self, cls: type | Any, *__args, **__kwargs):
return self

def resolve(self, cls: type[_T] | Any, *__args, **__kwargs) -> _T:
instance = self.__module.get_instance(cls)

if instance is None:
raise NoInjectable(cls)

return instance
return self.__module.get_instance(cls, none=False)
13 changes: 13 additions & 0 deletions injection/integrations/typer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typer import Option

__all__ = ("ignore",)


def ignore():
"""Typer option for the CLI to ignore this option and replace it with `None`."""

return Option(
default_factory=str,
parser=lambda _: None,
hidden=True,
)
Loading