smalldi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smalldi-0.1.0/.gitignore +5 -0
- smalldi-0.1.0/CHANGELOG.md +3 -0
- smalldi-0.1.0/LICENSE +21 -0
- smalldi-0.1.0/PKG-INFO +105 -0
- smalldi-0.1.0/README.md +58 -0
- smalldi-0.1.0/pyproject.toml +50 -0
- smalldi-0.1.0/src/smalldi/__init__.py +51 -0
- smalldi-0.1.0/src/smalldi/annotation.py +47 -0
- smalldi-0.1.0/src/smalldi/py.typed +0 -0
- smalldi-0.1.0/src/smalldi/wrappers.py +8 -0
- smalldi-0.1.0/src/tests/README.md +9 -0
- smalldi-0.1.0/src/tests/__init__.py +1 -0
- smalldi-0.1.0/src/tests/conftest.py +17 -0
- smalldi-0.1.0/src/tests/test_injector.py +87 -0
- smalldi-0.1.0/src/tests/test_integration.py +87 -0
- smalldi-0.1.0/src/tests/test_provide.py +35 -0
- smalldi-0.1.0/src/tests/test_wrappers.py +41 -0
smalldi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anna-Sofia Kasierocka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
smalldi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smalldi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small, annotation-driven dependency injection for Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/0xf104a/smalldi
|
|
6
|
+
Project-URL: Documentation, https://github.com/0xf104a/smalldi#readme
|
|
7
|
+
Project-URL: Source, https://github.com/0xf104a/smalldi
|
|
8
|
+
Project-URL: Issues, https://github.com/0xf104a/smalldi/issues
|
|
9
|
+
Author-email: Anna-Sofia Kasierocka <f104a@f104a.io>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 Anna-Sofia Kasierocka
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: annotations,dependency injection,di,typing
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.11
|
|
45
|
+
Requires-Dist: typing-extensions>=4.5; python_version >= '3.11'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# Small DI
|
|
49
|
+
The smallest dependency injection mechanism possible in python.
|
|
50
|
+
SmallDI provides you with intuitive and simple interface for doing dependency
|
|
51
|
+
injections in your project.
|
|
52
|
+
|
|
53
|
+
# Example usage
|
|
54
|
+
```python
|
|
55
|
+
import random
|
|
56
|
+
|
|
57
|
+
from smalldi import Injector
|
|
58
|
+
from smalldi.annotation import Provide
|
|
59
|
+
|
|
60
|
+
# Lets create some service
|
|
61
|
+
@Injector.singleton
|
|
62
|
+
class MeowService:
|
|
63
|
+
_MEOWS = ["Meow", "Meow-meow", "Meowwwwww", "Mrromeowww", "Meeeeoooow"]
|
|
64
|
+
def __init__(self):
|
|
65
|
+
print("Meow-meow! Meowing service is initialized")
|
|
66
|
+
|
|
67
|
+
def meow(self):
|
|
68
|
+
print(random.choice(self._MEOWS))
|
|
69
|
+
|
|
70
|
+
# Now lets make purring service.
|
|
71
|
+
# But cats do not purr without telling meow!(at least in this test)
|
|
72
|
+
# So its time to inject dependency
|
|
73
|
+
@Injector.singleton
|
|
74
|
+
class PurrService:
|
|
75
|
+
_PURRS = ["Purrrrr", "Purr-purr"]
|
|
76
|
+
|
|
77
|
+
@Injector.inject
|
|
78
|
+
def __init__(self, meow_service: Provide[MeowService]):
|
|
79
|
+
self.meow_service = meow_service
|
|
80
|
+
print("Purr-purr! Purring service is initialized")
|
|
81
|
+
|
|
82
|
+
def purr(self):
|
|
83
|
+
self.meow_service.meow()
|
|
84
|
+
print(random.choice(self._PURRS))
|
|
85
|
+
|
|
86
|
+
# Now lets put it all together.
|
|
87
|
+
# Ask our services to meow and then purr
|
|
88
|
+
@Injector.inject
|
|
89
|
+
def main(meow_service: Provide[MeowService], purr_service: Provide[PurrService]):
|
|
90
|
+
meow_service.meow()
|
|
91
|
+
purr_service.purr()
|
|
92
|
+
|
|
93
|
+
if __name__ == '__main__':
|
|
94
|
+
main()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
# Library logic
|
|
98
|
+
## Injector
|
|
99
|
+
Injector is a static class(i.e. one that should never be instantiated) storing all singletons
|
|
100
|
+
and responsible for injecting those singletons to a functions.
|
|
101
|
+
|
|
102
|
+
## Provide
|
|
103
|
+
`Provide[T]` is an annotation for injector telling it that instead of this argument
|
|
104
|
+
instance of `T` should be passed. Caller of function with `Provide[T]` may explicitly
|
|
105
|
+
override argument annotated with `Provide[T]` by directly passing `annotated_di_arg=my_value`.
|
smalldi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Small DI
|
|
2
|
+
The smallest dependency injection mechanism possible in python.
|
|
3
|
+
SmallDI provides you with intuitive and simple interface for doing dependency
|
|
4
|
+
injections in your project.
|
|
5
|
+
|
|
6
|
+
# Example usage
|
|
7
|
+
```python
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
from smalldi import Injector
|
|
11
|
+
from smalldi.annotation import Provide
|
|
12
|
+
|
|
13
|
+
# Lets create some service
|
|
14
|
+
@Injector.singleton
|
|
15
|
+
class MeowService:
|
|
16
|
+
_MEOWS = ["Meow", "Meow-meow", "Meowwwwww", "Mrromeowww", "Meeeeoooow"]
|
|
17
|
+
def __init__(self):
|
|
18
|
+
print("Meow-meow! Meowing service is initialized")
|
|
19
|
+
|
|
20
|
+
def meow(self):
|
|
21
|
+
print(random.choice(self._MEOWS))
|
|
22
|
+
|
|
23
|
+
# Now lets make purring service.
|
|
24
|
+
# But cats do not purr without telling meow!(at least in this test)
|
|
25
|
+
# So its time to inject dependency
|
|
26
|
+
@Injector.singleton
|
|
27
|
+
class PurrService:
|
|
28
|
+
_PURRS = ["Purrrrr", "Purr-purr"]
|
|
29
|
+
|
|
30
|
+
@Injector.inject
|
|
31
|
+
def __init__(self, meow_service: Provide[MeowService]):
|
|
32
|
+
self.meow_service = meow_service
|
|
33
|
+
print("Purr-purr! Purring service is initialized")
|
|
34
|
+
|
|
35
|
+
def purr(self):
|
|
36
|
+
self.meow_service.meow()
|
|
37
|
+
print(random.choice(self._PURRS))
|
|
38
|
+
|
|
39
|
+
# Now lets put it all together.
|
|
40
|
+
# Ask our services to meow and then purr
|
|
41
|
+
@Injector.inject
|
|
42
|
+
def main(meow_service: Provide[MeowService], purr_service: Provide[PurrService]):
|
|
43
|
+
meow_service.meow()
|
|
44
|
+
purr_service.purr()
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
main()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
# Library logic
|
|
51
|
+
## Injector
|
|
52
|
+
Injector is a static class(i.e. one that should never be instantiated) storing all singletons
|
|
53
|
+
and responsible for injecting those singletons to a functions.
|
|
54
|
+
|
|
55
|
+
## Provide
|
|
56
|
+
`Provide[T]` is an annotation for injector telling it that instead of this argument
|
|
57
|
+
instance of `T` should be passed. Caller of function with `Provide[T]` may explicitly
|
|
58
|
+
override argument annotated with `Provide[T]` by directly passing `annotated_di_arg=my_value`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smalldi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Small, annotation-driven dependency injection for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Anna-Sofia Kasierocka", email = "f104a@f104a.io" }]
|
|
13
|
+
keywords = ["dependency injection", "di", "typing", "annotations"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"typing-extensions>=4.5; python_version>='3.11'"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/0xf104a/smalldi"
|
|
33
|
+
Documentation = "https://github.com/0xf104a/smalldi#readme"
|
|
34
|
+
Source = "https://github.com/0xf104a/smalldi"
|
|
35
|
+
Issues = "https://github.com/0xf104a/smalldi/issues"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/smalldi"]
|
|
39
|
+
include = [
|
|
40
|
+
"src/smalldi/py.typed",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = [
|
|
45
|
+
"/src",
|
|
46
|
+
"/README.md",
|
|
47
|
+
"/LICENSE",
|
|
48
|
+
"/pyproject.toml",
|
|
49
|
+
"/CHANGELOG.md",
|
|
50
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from smalldi.wrappers import staticclass
|
|
4
|
+
from smalldi.annotation import _Provide, Provide
|
|
5
|
+
|
|
6
|
+
__author__ = "Anna-Sofia Kasierocka"
|
|
7
|
+
__email__ = "f104a@f104a.io"
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["Injector", "Provide"]
|
|
10
|
+
|
|
11
|
+
@staticclass
|
|
12
|
+
class Injector:
|
|
13
|
+
"""
|
|
14
|
+
The injector class handles all the dependency injections.
|
|
15
|
+
"""
|
|
16
|
+
singletons_available = {}
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def inject(cls, fn):
|
|
20
|
+
"""
|
|
21
|
+
Injects dependencies into function.
|
|
22
|
+
:param fn: function to inject dependencies into
|
|
23
|
+
:return: function with injected dependencies
|
|
24
|
+
"""
|
|
25
|
+
kwargs_ext = dict()
|
|
26
|
+
for name, tp in _Provide.iter_annotations(fn):
|
|
27
|
+
if tp not in cls.singletons_available:
|
|
28
|
+
raise TypeError(f"Singleton {tp} is not available")
|
|
29
|
+
kwargs_ext[name] = cls.singletons_available[tp]
|
|
30
|
+
|
|
31
|
+
@functools.wraps(fn)
|
|
32
|
+
def wrapped_fn(*args, **kwargs):
|
|
33
|
+
for name, value in kwargs_ext.items():
|
|
34
|
+
if name not in kwargs:
|
|
35
|
+
kwargs[name] = value
|
|
36
|
+
return fn(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
return wrapped_fn
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def singleton(cls, target_cls):
|
|
42
|
+
"""
|
|
43
|
+
Marks class as singleton.
|
|
44
|
+
Singleton classes are instantiated only once by the injector.
|
|
45
|
+
Also their __init__ function should have no arguments or be annotated
|
|
46
|
+
with @Injector.inject, so it would
|
|
47
|
+
:param target_cls: Class which to be marked as injectable singleton
|
|
48
|
+
:return: None
|
|
49
|
+
"""
|
|
50
|
+
cls.singletons_available[target_cls] = target_cls()
|
|
51
|
+
return target_cls
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import TypeVar, Generic, get_origin, get_args, Iterator, Callable, Any, TypeAlias, Annotated
|
|
3
|
+
|
|
4
|
+
_T = TypeVar("_T")
|
|
5
|
+
|
|
6
|
+
class _Provide(Generic[_T]):
|
|
7
|
+
"""Provide annotation tells smalldi to find instance of T and inject it into function"""
|
|
8
|
+
@staticmethod
|
|
9
|
+
def unwrap(tp: object) -> type:
|
|
10
|
+
"""
|
|
11
|
+
Unwraps annotation to get inner type.
|
|
12
|
+
:param tp: annotation itself
|
|
13
|
+
:return: inner type T
|
|
14
|
+
"""
|
|
15
|
+
origin = get_origin(tp)
|
|
16
|
+
if origin is _Provide:
|
|
17
|
+
(inner,) = get_args(tp)
|
|
18
|
+
return inner
|
|
19
|
+
|
|
20
|
+
if origin is Annotated:
|
|
21
|
+
args = get_args(tp)
|
|
22
|
+
inner, *meta = args
|
|
23
|
+
if any(m is _Provide for m in meta):
|
|
24
|
+
return inner
|
|
25
|
+
|
|
26
|
+
raise TypeError(f"Expected Provide[T], got {tp!r}")
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def iter_annotations(func: Callable) -> Iterator[Any]:
|
|
30
|
+
"""
|
|
31
|
+
Provides an iterator that extracts and yields annotations of type `Provide`
|
|
32
|
+
from the parameters of a given function's signature.
|
|
33
|
+
:param func: The function whose parameter annotations will be inspected.
|
|
34
|
+
:type func: Callable
|
|
35
|
+
:return: An iterator over the unwrapped annotations of type `Provide` found
|
|
36
|
+
in the function's parameters.
|
|
37
|
+
:rtype: Iterator[str, Any]
|
|
38
|
+
"""
|
|
39
|
+
signature = inspect.signature(func)
|
|
40
|
+
for name, param in signature.parameters.items():
|
|
41
|
+
try:
|
|
42
|
+
inner = _Provide.unwrap(param.annotation)
|
|
43
|
+
yield name, inner
|
|
44
|
+
except TypeError:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
Provide: TypeAlias = Annotated[_T, _Provide]
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Tests for smalldi
|
|
2
|
+
|
|
3
|
+
This module contains tests for a smalldi library with the use of pytest
|
|
4
|
+
|
|
5
|
+
## Test structure
|
|
6
|
+
- `test_provide.py` - Tests for class `Provide`
|
|
7
|
+
- `test_injector.py` - Тests for class `Injector`
|
|
8
|
+
- `test_wrappers.py` - Tests for wrappers (currently only `staticclass`)
|
|
9
|
+
- `test_integration.py` - Library-level tests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# tests package
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from smalldi import Injector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(autouse=False)
|
|
8
|
+
def reset_injector():
|
|
9
|
+
"""Reset Injector state before and after each test"""
|
|
10
|
+
old_singletons: Dict[Any, Any] = Injector.singletons_available.copy()
|
|
11
|
+
|
|
12
|
+
Injector.singletons_available.clear()
|
|
13
|
+
|
|
14
|
+
yield
|
|
15
|
+
|
|
16
|
+
Injector.singletons_available.clear()
|
|
17
|
+
Injector.singletons_available.update(old_singletons)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
from smalldi import Injector
|
|
6
|
+
from smalldi.annotation import _Provide
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def reset_injector():
|
|
11
|
+
"""Reset Injector state before each test"""
|
|
12
|
+
old_singletons = Injector.singletons_available.copy()
|
|
13
|
+
Injector.singletons_available.clear()
|
|
14
|
+
|
|
15
|
+
yield
|
|
16
|
+
|
|
17
|
+
# Відновлюємо стан після тесту
|
|
18
|
+
Injector.singletons_available.clear()
|
|
19
|
+
Injector.singletons_available.update(old_singletons)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_singleton_registration(reset_injector):
|
|
23
|
+
"""Test that singleton decorator registers class instance"""
|
|
24
|
+
assert len(Injector.singletons_available) == 0
|
|
25
|
+
|
|
26
|
+
@Injector.singleton
|
|
27
|
+
class TestService:
|
|
28
|
+
def hello(self):
|
|
29
|
+
return "Hello"
|
|
30
|
+
|
|
31
|
+
assert len(Injector.singletons_available) == 1
|
|
32
|
+
assert TestService in Injector.singletons_available
|
|
33
|
+
assert isinstance(Injector.singletons_available[TestService], TestService)
|
|
34
|
+
|
|
35
|
+
service = Injector.singletons_available[TestService]
|
|
36
|
+
assert service.hello() == "Hello"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_singleton_single_instance(reset_injector):
|
|
40
|
+
"""Test that singleton creates only one instance"""
|
|
41
|
+
counter = 0
|
|
42
|
+
|
|
43
|
+
@Injector.singleton
|
|
44
|
+
class TestService:
|
|
45
|
+
def __init__(self):
|
|
46
|
+
nonlocal counter
|
|
47
|
+
counter += 1
|
|
48
|
+
|
|
49
|
+
assert counter == 1
|
|
50
|
+
|
|
51
|
+
Injector.singleton(TestService)
|
|
52
|
+
assert counter == 2 # Це повинно бути 1, але поточна реалізація створить новий екземпляр
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_inject_basic(reset_injector):
|
|
56
|
+
"""Test basic dependency injection"""
|
|
57
|
+
@Injector.singleton
|
|
58
|
+
class TestService:
|
|
59
|
+
def hello(self):
|
|
60
|
+
return "Hello"
|
|
61
|
+
|
|
62
|
+
@Injector.inject
|
|
63
|
+
def test_function(service: _Provide[TestService]):
|
|
64
|
+
return service.hello()
|
|
65
|
+
|
|
66
|
+
result = test_function()
|
|
67
|
+
assert result == "Hello"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_inject_multiple_dependencies(reset_injector):
|
|
71
|
+
"""Test injection of multiple dependencies"""
|
|
72
|
+
@Injector.singleton
|
|
73
|
+
class ServiceA:
|
|
74
|
+
def value(self):
|
|
75
|
+
return "A"
|
|
76
|
+
|
|
77
|
+
@Injector.singleton
|
|
78
|
+
class ServiceB:
|
|
79
|
+
def value(self):
|
|
80
|
+
return "B"
|
|
81
|
+
|
|
82
|
+
@Injector.inject
|
|
83
|
+
def test_function(service_a: _Provide[ServiceA], service_b: _Provide[ServiceB]):
|
|
84
|
+
return service_a.value() + service_b.value()
|
|
85
|
+
|
|
86
|
+
result = test_function()
|
|
87
|
+
assert result == "AB"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from smalldi import Injector
|
|
5
|
+
from smalldi.annotation import Provide
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def reset_injector():
|
|
10
|
+
"""Reset Injector state before each test"""
|
|
11
|
+
old_singletons = Injector.singletons_available.copy()
|
|
12
|
+
Injector.singletons_available.clear()
|
|
13
|
+
yield
|
|
14
|
+
Injector.singletons_available.clear()
|
|
15
|
+
Injector.singletons_available.update(old_singletons)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_simple_injection(reset_injector):
|
|
19
|
+
"""Test simple dependency injection in real code"""
|
|
20
|
+
|
|
21
|
+
@Injector.singleton
|
|
22
|
+
class ConfigService:
|
|
23
|
+
def get_config(self, key: str) -> str:
|
|
24
|
+
return f"config:{key}"
|
|
25
|
+
|
|
26
|
+
@Injector.singleton
|
|
27
|
+
class UserService:
|
|
28
|
+
@Injector.inject
|
|
29
|
+
def __init__(self, config: Provide[ConfigService]):
|
|
30
|
+
self.config = config
|
|
31
|
+
|
|
32
|
+
def get_user(self, user_id: int) -> Dict[str, str]:
|
|
33
|
+
api_key = self.config.get_config("api_key")
|
|
34
|
+
return {"id": str(user_id), "name": f"User_{user_id}", "api_key": api_key}
|
|
35
|
+
|
|
36
|
+
@Injector.inject
|
|
37
|
+
def get_user_handler(user_id: int, user_service: Provide[UserService]) -> Dict[str, str]:
|
|
38
|
+
return user_service.get_user(user_id)
|
|
39
|
+
|
|
40
|
+
result = get_user_handler(user_id=123)
|
|
41
|
+
assert result["id"] == "123"
|
|
42
|
+
assert result["name"] == "User_123"
|
|
43
|
+
assert result["api_key"] == "config:api_key"
|
|
44
|
+
|
|
45
|
+
def test_nested_injection(reset_injector, monkeypatch):
|
|
46
|
+
"""Test nested dependency injection"""
|
|
47
|
+
@Injector.singleton
|
|
48
|
+
class DatabaseService:
|
|
49
|
+
def query(self, sql: str) -> List[Dict[str, str]]:
|
|
50
|
+
return [{"result": f"db:{sql}"}]
|
|
51
|
+
|
|
52
|
+
@Injector.singleton
|
|
53
|
+
class LogService:
|
|
54
|
+
def log(self, message: str) -> None:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@Injector.singleton
|
|
58
|
+
class UserRepository:
|
|
59
|
+
@Injector.inject
|
|
60
|
+
def __init__(self, db: Provide[DatabaseService], logger: Provide[LogService]):
|
|
61
|
+
self.db = db
|
|
62
|
+
self.logger = logger
|
|
63
|
+
|
|
64
|
+
def find_user(self, user_id: int) -> Optional[Dict[str, str]]:
|
|
65
|
+
self.logger.log(f"Finding user {user_id}")
|
|
66
|
+
results = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
|
|
67
|
+
return results[0] if results else None
|
|
68
|
+
|
|
69
|
+
@Injector.singleton
|
|
70
|
+
class UserService:
|
|
71
|
+
@Injector.inject
|
|
72
|
+
def __init__(self, repository: Provide[UserRepository]):
|
|
73
|
+
self.repository = repository
|
|
74
|
+
|
|
75
|
+
def get_user_data(self, user_id: int) -> Dict[str, str]:
|
|
76
|
+
user = self.repository.find_user(user_id)
|
|
77
|
+
if not user:
|
|
78
|
+
return {"error": "User not found"}
|
|
79
|
+
return user
|
|
80
|
+
|
|
81
|
+
@Injector.inject
|
|
82
|
+
def user_api(user_id: int, service: Provide[UserService]) -> Dict[str, str]:
|
|
83
|
+
return service.get_user_data(user_id)
|
|
84
|
+
|
|
85
|
+
result = user_api(user_id=42)
|
|
86
|
+
assert "result" in result
|
|
87
|
+
assert result["result"] == "db:SELECT * FROM users WHERE id = 42"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from typing import Any, List, Dict
|
|
3
|
+
|
|
4
|
+
from smalldi.annotation import _Provide, Provide
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_provide_unwrap_valid():
|
|
8
|
+
"""Test unwrapping valid Provide annotations"""
|
|
9
|
+
assert _Provide.unwrap(Provide[str]) is str
|
|
10
|
+
assert _Provide.unwrap(Provide[int]) is int
|
|
11
|
+
assert _Provide.unwrap(Provide[List[int]]) is List[int]
|
|
12
|
+
assert _Provide.unwrap(Provide[Dict[str, Any]]) is Dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_provide_unwrap_invalid():
|
|
16
|
+
"""Test unwrapping invalid annotations raises TypeError"""
|
|
17
|
+
with pytest.raises(TypeError):
|
|
18
|
+
_Provide.unwrap(str)
|
|
19
|
+
|
|
20
|
+
with pytest.raises(TypeError):
|
|
21
|
+
_Provide.unwrap(int)
|
|
22
|
+
|
|
23
|
+
with pytest.raises(TypeError):
|
|
24
|
+
_Provide.unwrap(List[int])
|
|
25
|
+
|
|
26
|
+
with pytest.raises(TypeError):
|
|
27
|
+
_Provide.unwrap(None)
|
|
28
|
+
|
|
29
|
+
def test_iter_annotations_empty():
|
|
30
|
+
"""Test iteration over function without Provide annotations"""
|
|
31
|
+
def func(a: str, b: int):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
annotations = list(_Provide.iter_annotations(func))
|
|
35
|
+
assert len(annotations) == 0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from smalldi.wrappers import staticclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_staticclass_basic():
|
|
7
|
+
"""Test that staticclass prevents instantiation"""
|
|
8
|
+
@staticclass
|
|
9
|
+
class TestClass:
|
|
10
|
+
@classmethod
|
|
11
|
+
def class_method(cls):
|
|
12
|
+
return "class method"
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def static_method():
|
|
16
|
+
return "static method"
|
|
17
|
+
|
|
18
|
+
with pytest.raises(TypeError):
|
|
19
|
+
TestClass()
|
|
20
|
+
|
|
21
|
+
assert TestClass.class_method() == "class method"
|
|
22
|
+
assert TestClass.static_method() == "static method"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_staticclass_preserves_metadata():
|
|
26
|
+
"""Test that staticclass preserves class metadata"""
|
|
27
|
+
@staticclass
|
|
28
|
+
class TestClass:
|
|
29
|
+
"""Test docstring"""
|
|
30
|
+
class_var = "class variable"
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def method(cls):
|
|
34
|
+
"""Method docstring"""
|
|
35
|
+
return cls.class_var
|
|
36
|
+
|
|
37
|
+
assert TestClass.__name__ == "TestClass"
|
|
38
|
+
assert TestClass.__doc__ == "Test docstring"
|
|
39
|
+
assert TestClass.class_var == "class variable"
|
|
40
|
+
assert TestClass.method.__doc__ == "Method docstring"
|
|
41
|
+
assert TestClass.method() == "class variable"
|