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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ if __name__ == "__main__":
* [**Entrypoint**](https://github.com/100nm/python-injection/tree/prod/documentation/entrypoint.md)
* [**Integrations**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations)
* [**FastAPI**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations/fastapi.md)
* [**What if my framework isn't listed?**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations/unlisted-framework.md)
* [**Concrete example**](https://github.com/100nm/python-injection-example)
38 changes: 38 additions & 0 deletions documentation/integrations/unlisted-framework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# What if my framework isn't listed?

You like `python-injection`, but your framework isn't officially supported? Don't worry, there are still several ways
to make it work.

## If your framework doesn't inspect function signatures

In most cases, if your framework doesn't inspect function signatures, you can use the `@inject` decorator without any
issues.

## If your framework inspects function signatures

If your framework inspects function signatures, things get a bit trickier. This is because you'll need to **perform
dependency injection first, then call the function**, which isn't always compatible with how decorators work on regular
functions.

To solve this, you can define a class with a `__call__` method (where dependencies are injected), and use the
`asfunction` decorator to turn it into a function.

The resulting function will have the same signature as the `__call__` method, but without the `self` parameter.

Example:

```python
from typing import NamedTuple
from injection import asfunction

@asfunction
class do_something(NamedTuple):
service: MyService

def __call__(self):
self.service.do_work()
```

## Need more than these tools?

[**Feel free to start a discussion here.**](https://github.com/100nm/python-injection/discussions/new/choose)
2 changes: 2 additions & 0 deletions injection/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._core.asfunction import asfunction
from ._core.descriptors import LazyInstance
from ._core.injectables import Injectable
from ._core.module import Mode, Module, Priority, mod
Expand All @@ -18,6 +19,7 @@
"afind_instance",
"aget_instance",
"aget_lazy_instance",
"asfunction",
"constant",
"define_scope",
"find_instance",
Expand Down
16 changes: 16 additions & 0 deletions injection/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ set_constant = __MODULE.set_constant
should_be_injectable = __MODULE.should_be_injectable
singleton = __MODULE.singleton

@overload
def asfunction[**P, T](
wrapped: type[Callable[P, T]],
/,
*,
module: Module = ...,
threadsafe: bool = ...,
) -> Callable[P, T]: ...
@overload
def asfunction[**P, T](
wrapped: None = ...,
/,
*,
module: Module = ...,
threadsafe: bool = ...,
) -> Callable[[type[Callable[P, T]]], Callable[P, T]]: ...
@asynccontextmanager
def adefine_scope(
name: str,
Expand Down
47 changes: 47 additions & 0 deletions injection/_core/asfunction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from collections.abc import Callable
from functools import wraps
from inspect import iscoroutinefunction
from typing import Any

from injection._core.common.asynchronous import Caller
from injection._core.module import Module, mod


def asfunction[**P, T](
wrapped: type[Callable[P, T]] | None = None,
/,
*,
module: Module | None = None,
threadsafe: bool = False,
) -> Any:
module = module or mod()

def decorator(wp: type[Callable[P, T]]) -> Callable[P, T]:
get_method = wp.__call__.__get__
method = get_method(NotImplemented)
factory: Caller[..., Callable[P, T]] = module.make_injected_function(
wp,
threadsafe=threadsafe,
).__inject_metadata__

wrapper: Callable[P, T]

if iscoroutinefunction(method):

@wraps(method)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
self = await factory.acall()
return await get_method(self)(*args, **kwargs)

else:

@wraps(method)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
self = factory.call()
return get_method(self)(*args, **kwargs)

wrapper.__name__ = wp.__name__
wrapper.__qualname__ = wp.__qualname__
return wrapper

return decorator(wrapped) if wrapped else decorator
34 changes: 34 additions & 0 deletions tests/core/test_asfunction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import NamedTuple

from injection import asfunction


class TestAsFunction:
def test_asfunction_with_sync_call_method(self, module):
@module.injectable
class Dependency: ...

@asfunction(module=module)
class SyncFunction(NamedTuple):
dependency: Dependency

def __call__(self):
return self.dependency

assert isinstance(SyncFunction(), Dependency)

async def test_asfunction_with_async_call_method(self, module):
class Dependency: ...

@module.injectable
async def dependency_recipe() -> Dependency:
return Dependency()

@asfunction(module=module)
class AsyncFunction(NamedTuple):
dependency: Dependency

async def __call__(self):
return self.dependency

assert isinstance(await AsyncFunction(), Dependency)
42 changes: 21 additions & 21 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.