Skip to content

Commit c189f44

Browse files
authored
feat: ✨ Allow Pydantic injectable
1 parent 233a04c commit c189f44

File tree

7 files changed

+204
-60
lines changed

7 files changed

+204
-60
lines changed

documentation/basic-usage.md

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ If you wish to inject a new instance each time, use `injectable` decorator.
1717
```python
1818
from injection import injectable
1919

20-
2120
@injectable
2221
class Injectable:
2322
""" class implementation """
2423
```
2524

25+
> **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
26+
2627
## Inject an instance
2728

2829
To inject one or several instances, use `inject` decorator.
@@ -50,6 +51,9 @@ class DataClass:
5051
instance: Injectable = ...
5152
```
5253

54+
> **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
55+
> dependencies.
56+
5357
## Inheritance
5458

5559
In the case of inheritance, you can use the decorator parameter `on` to link the injection to one or several other
@@ -99,16 +103,3 @@ from injection import singleton
99103
def my_recipe() -> Singleton:
100104
""" recipe implementation """
101105
```
102-
103-
## Auto inject
104-
105-
By default, `injectable` and `singleton` decorators will automatically apply `@inject` to the decorated class or
106-
function. To disable it, set the `auto_inject` parameter to `False`.
107-
108-
```python
109-
from injection import singleton
110-
111-
@singleton(auto_inject=False)
112-
class Singleton:
113-
""" class implementation """
114-
```

injection/__init__.pyi

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,23 @@ class Module:
3636
/,
3737
*,
3838
on: type | Iterable[type] = ...,
39-
auto_inject: bool = ...,
4039
):
4140
"""
4241
Decorator applicable to a class or function. It is used to indicate how the
4342
injectable will be constructed. At injection time, a new instance will be
44-
injected each time. Automatically injects constructor dependencies, can be
45-
disabled with `auto_inject=False`.
43+
injected each time.
4644
"""
4745
def singleton(
4846
self,
4947
wrapped: Callable[..., Any] = ...,
5048
/,
5149
*,
5250
on: type | Iterable[type] = ...,
53-
auto_inject: bool = ...,
5451
):
5552
"""
5653
Decorator applicable to a class or function. It is used to indicate how the
5754
singleton will be constructed. At injection time, the injected instance will
58-
always be the same. Automatically injects constructor dependencies, can be
59-
disabled with `auto_inject=False`.
55+
always be the same.
6056
"""
6157
def get_instance(self, cls: type[_T]) -> _T | None:
6258
"""

injection/core/module.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -547,12 +547,8 @@ def __call__(
547547
/,
548548
*,
549549
on: type | Iterable[type] = None,
550-
auto_inject: bool = True,
551550
):
552551
def decorator(wp):
553-
if auto_inject:
554-
wp = self.__module.inject(wp)
555-
556552
@lambda fn: fn()
557553
def classes():
558554
if cls := self.__get_target_class(wp):
@@ -565,7 +561,12 @@ def classes():
565561
else:
566562
yield on
567563

568-
injectable = self.__injectable_type(wp)
564+
@self.__module.inject
565+
@wraps(wp, updated=())
566+
def factory(*args, **kwargs):
567+
return wp(*args, **kwargs)
568+
569+
injectable = self.__injectable_type(factory)
569570
self.__module.update(classes, injectable)
570571

571572
return wp

poetry.lock

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

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ black = "*"
1717
blacksheep = "^2.0.4"
1818
flake8 = "*"
1919
isort = "*"
20+
pydantic = "^2.5.3"
2021
pytest = "*"
2122
pytest-asyncio = "*"
2223
pytest-cov = "*"

tests/test_injectable.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22

33
import pytest
4+
from pydantic import BaseModel
45

56
from injection import get_instance, injectable
67

@@ -65,28 +66,32 @@ class C(B):
6566
assert isinstance(b, C)
6667
assert a is not b
6768

68-
def test_injectable_without_auto_inject_raise_type_error(self):
69+
def test_injectable_with_inject(self):
6970
@injectable
7071
class A:
7172
...
7273

73-
@injectable(auto_inject=False)
74+
@injectable
7475
class B:
75-
def __init__(self, a: A):
76-
raise NotImplementedError
76+
def __init__(self, __a: A):
77+
self.a = __a
7778

78-
with pytest.raises(TypeError):
79-
get_instance(B)
79+
a = get_instance(A)
80+
b = get_instance(B)
81+
assert isinstance(a, A)
82+
assert isinstance(b, B)
83+
assert isinstance(b.a, A)
84+
assert a is not b.a
8085

81-
def test_injectable_with_auto_inject(self):
86+
def test_injectable_with_dataclass_and_inject(self):
8287
@injectable
8388
class A:
8489
...
8590

86-
@injectable(auto_inject=True)
91+
@injectable
92+
@dataclass(frozen=True, slots=True)
8793
class B:
88-
def __init__(self, __a: A):
89-
self.a = __a
94+
a: A
9095

9196
a = get_instance(A)
9297
b = get_instance(B)
@@ -95,14 +100,13 @@ def __init__(self, __a: A):
95100
assert isinstance(b.a, A)
96101
assert a is not b.a
97102

98-
def test_injectable_with_dataclass_and_auto_inject(self):
103+
def test_injectable_with_pydantic_model_and_inject(self):
99104
@injectable
100-
class A:
105+
class A(BaseModel):
101106
...
102107

103-
@injectable(auto_inject=True)
104-
@dataclass(frozen=True, slots=True)
105-
class B:
108+
@injectable
109+
class B(BaseModel):
106110
a: A
107111

108112
a = get_instance(A)
@@ -112,15 +116,15 @@ class B:
112116
assert isinstance(b.a, A)
113117
assert a is not b.a
114118

115-
def test_injectable_with_recipe_and_auto_inject(self):
119+
def test_injectable_with_recipe_and_inject(self):
116120
@injectable
117121
class A:
118122
...
119123

120124
class B:
121125
...
122126

123-
@injectable(auto_inject=True)
127+
@injectable
124128
def recipe(__a: A) -> B:
125129
assert isinstance(__a, A)
126130
assert __a is not a

tests/test_singleton.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22

33
import pytest
4+
from pydantic import BaseModel
45

56
from injection import get_instance, singleton
67

@@ -65,28 +66,32 @@ class C(B):
6566
assert isinstance(b, C)
6667
assert a is b
6768

68-
def test_injectable_without_auto_inject_raise_type_error(self):
69+
def test_singleton_with_inject(self):
6970
@singleton
7071
class A:
7172
...
7273

73-
@singleton(auto_inject=False)
74+
@singleton
7475
class B:
75-
def __init__(self, a: A):
76-
raise NotImplementedError
76+
def __init__(self, __a: A):
77+
self.a = __a
7778

78-
with pytest.raises(TypeError):
79-
get_instance(B)
79+
a = get_instance(A)
80+
b = get_instance(B)
81+
assert isinstance(a, A)
82+
assert isinstance(b, B)
83+
assert isinstance(b.a, A)
84+
assert a is b.a
8085

81-
def test_singleton_with_auto_inject(self):
86+
def test_singleton_with_dataclass_and_inject(self):
8287
@singleton
8388
class A:
8489
...
8590

86-
@singleton(auto_inject=True)
91+
@singleton
92+
@dataclass(frozen=True, slots=True)
8793
class B:
88-
def __init__(self, __a: A):
89-
self.a = __a
94+
a: A
9095

9196
a = get_instance(A)
9297
b = get_instance(B)
@@ -95,14 +100,13 @@ def __init__(self, __a: A):
95100
assert isinstance(b.a, A)
96101
assert a is b.a
97102

98-
def test_singleton_with_dataclass_and_auto_inject(self):
103+
def test_singleton_with_pydantic_model_and_inject(self):
99104
@singleton
100-
class A:
105+
class A(BaseModel):
101106
...
102107

103-
@singleton(auto_inject=True)
104-
@dataclass(frozen=True, slots=True)
105-
class B:
108+
@singleton
109+
class B(BaseModel):
106110
a: A
107111

108112
a = get_instance(A)
@@ -112,15 +116,15 @@ class B:
112116
assert isinstance(b.a, A)
113117
assert a is b.a
114118

115-
def test_singleton_with_recipe_and_auto_inject(self):
119+
def test_singleton_with_recipe_and_inject(self):
116120
@singleton
117121
class A:
118122
...
119123

120124
class B:
121125
...
122126

123-
@singleton(auto_inject=True)
127+
@singleton
124128
def recipe(__a: A) -> B:
125129
assert isinstance(__a, A)
126130
assert __a is a

0 commit comments

Comments
 (0)