Skip to content

Commit aef964d

Browse files
authored
feat: ✨ Introduce @asfunction
1 parent 7e3207c commit aef964d

7 files changed

Lines changed: 159 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,5 @@ if __name__ == "__main__":
7575
* [**Entrypoint**](https://github.com/100nm/python-injection/tree/prod/documentation/entrypoint.md)
7676
* [**Integrations**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations)
7777
* [**FastAPI**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations/fastapi.md)
78+
* [**What if my framework isn't listed?**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations/unlisted-framework.md)
7879
* [**Concrete example**](https://github.com/100nm/python-injection-example)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# What if my framework isn't listed?
2+
3+
You like `python-injection`, but your framework isn't officially supported? Don't worry, there are still several ways
4+
to make it work.
5+
6+
## If your framework doesn't inspect function signatures
7+
8+
In most cases, if your framework doesn't inspect function signatures, you can use the `@inject` decorator without any
9+
issues.
10+
11+
## If your framework inspects function signatures
12+
13+
If your framework inspects function signatures, things get a bit trickier. This is because you'll need to **perform
14+
dependency injection first, then call the function**, which isn't always compatible with how decorators work on regular
15+
functions.
16+
17+
To solve this, you can define a class with a `__call__` method (where dependencies are injected), and use the
18+
`asfunction` decorator to turn it into a function.
19+
20+
The resulting function will have the same signature as the `__call__` method, but without the `self` parameter.
21+
22+
Example:
23+
24+
```python
25+
from typing import NamedTuple
26+
from injection import asfunction
27+
28+
@asfunction
29+
class do_something(NamedTuple):
30+
service: MyService
31+
32+
def __call__(self):
33+
self.service.do_work()
34+
```
35+
36+
## Need more than these tools?
37+
38+
[**Feel free to start a discussion here.**](https://github.com/100nm/python-injection/discussions/new/choose)

injection/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._core.asfunction import asfunction
12
from ._core.descriptors import LazyInstance
23
from ._core.injectables import Injectable
34
from ._core.module import Mode, Module, Priority, mod
@@ -18,6 +19,7 @@
1819
"afind_instance",
1920
"aget_instance",
2021
"aget_lazy_instance",
22+
"asfunction",
2123
"constant",
2224
"define_scope",
2325
"find_instance",

injection/__init__.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ set_constant = __MODULE.set_constant
3030
should_be_injectable = __MODULE.should_be_injectable
3131
singleton = __MODULE.singleton
3232

33+
@overload
34+
def asfunction[**P, T](
35+
wrapped: type[Callable[P, T]],
36+
/,
37+
*,
38+
module: Module = ...,
39+
threadsafe: bool = ...,
40+
) -> Callable[P, T]: ...
41+
@overload
42+
def asfunction[**P, T](
43+
wrapped: None = ...,
44+
/,
45+
*,
46+
module: Module = ...,
47+
threadsafe: bool = ...,
48+
) -> Callable[[type[Callable[P, T]]], Callable[P, T]]: ...
3349
@asynccontextmanager
3450
def adefine_scope(
3551
name: str,

injection/_core/asfunction.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from collections.abc import Callable
2+
from functools import wraps
3+
from inspect import iscoroutinefunction
4+
from typing import Any
5+
6+
from injection._core.common.asynchronous import Caller
7+
from injection._core.module import Module, mod
8+
9+
10+
def asfunction[**P, T](
11+
wrapped: type[Callable[P, T]] | None = None,
12+
/,
13+
*,
14+
module: Module | None = None,
15+
threadsafe: bool = False,
16+
) -> Any:
17+
module = module or mod()
18+
19+
def decorator(wp: type[Callable[P, T]]) -> Callable[P, T]:
20+
get_method = wp.__call__.__get__
21+
method = get_method(NotImplemented)
22+
factory: Caller[..., Callable[P, T]] = module.make_injected_function(
23+
wp,
24+
threadsafe=threadsafe,
25+
).__inject_metadata__
26+
27+
wrapper: Callable[P, T]
28+
29+
if iscoroutinefunction(method):
30+
31+
@wraps(method)
32+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
33+
self = await factory.acall()
34+
return await get_method(self)(*args, **kwargs)
35+
36+
else:
37+
38+
@wraps(method)
39+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
40+
self = factory.call()
41+
return get_method(self)(*args, **kwargs)
42+
43+
wrapper.__name__ = wp.__name__
44+
wrapper.__qualname__ = wp.__qualname__
45+
return wrapper
46+
47+
return decorator(wrapped) if wrapped else decorator

tests/core/test_asfunction.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import NamedTuple
2+
3+
from injection import asfunction
4+
5+
6+
class TestAsFunction:
7+
def test_asfunction_with_sync_call_method(self, module):
8+
@module.injectable
9+
class Dependency: ...
10+
11+
@asfunction(module=module)
12+
class SyncFunction(NamedTuple):
13+
dependency: Dependency
14+
15+
def __call__(self):
16+
return self.dependency
17+
18+
assert isinstance(SyncFunction(), Dependency)
19+
20+
async def test_asfunction_with_async_call_method(self, module):
21+
class Dependency: ...
22+
23+
@module.injectable
24+
async def dependency_recipe() -> Dependency:
25+
return Dependency()
26+
27+
@asfunction(module=module)
28+
class AsyncFunction(NamedTuple):
29+
dependency: Dependency
30+
31+
async def __call__(self):
32+
return self.dependency
33+
34+
assert isinstance(await AsyncFunction(), Dependency)

uv.lock

Lines changed: 21 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)