modern-di 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of modern-di might be problematic. Click here for more details.

modern_di/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from modern_di.container import Container
2
+ from modern_di.graph import BaseGraph
3
+ from modern_di.scope import Scope
4
+
5
+
6
+ __all__ = [
7
+ "BaseGraph",
8
+ "Container",
9
+ "Scope",
10
+ "resolvers",
11
+ ]
modern_di/container.py ADDED
@@ -0,0 +1,109 @@
1
+ import contextlib
2
+ import enum
3
+ import types
4
+ import typing
5
+
6
+ from modern_di.resolver_state import ResolverState
7
+
8
+
9
+ if typing.TYPE_CHECKING:
10
+ import typing_extensions
11
+
12
+
13
+ T_co = typing.TypeVar("T_co", covariant=True)
14
+
15
+
16
+ class Container(contextlib.AbstractAsyncContextManager["Container"]):
17
+ __slots__ = "scope", "parent_container", "_is_async", "_resolver_states", "_overrides"
18
+
19
+ def __init__(self, *, scope: enum.IntEnum, parent_container: typing.Optional["Container"] = None) -> None:
20
+ if scope.value != 1 and parent_container is None:
21
+ msg = "Only first scope can be used without parent_container"
22
+ raise RuntimeError(msg)
23
+
24
+ self.scope = scope
25
+ self.parent_container = parent_container
26
+ self._is_async: bool | None = None
27
+ self._resolver_states: dict[str, ResolverState[typing.Any]] = {}
28
+ self._overrides: dict[str, typing.Any] = {}
29
+
30
+ def _exit(self) -> None:
31
+ self._is_async = None
32
+ self._resolver_states = {}
33
+
34
+ def _check_entered(self) -> None:
35
+ if self._is_async is None:
36
+ msg = "Enter the context first"
37
+ raise RuntimeError(msg)
38
+
39
+ def build_child_container(self) -> "typing_extensions.Self":
40
+ self._check_entered()
41
+
42
+ try:
43
+ new_scope = self.scope.__class__(self.scope.value + 1)
44
+ except ValueError as exc:
45
+ msg = f"Max scope is reached, {self.scope.name}"
46
+ raise RuntimeError(msg) from exc
47
+
48
+ return self.__class__(scope=new_scope, parent_container=self)
49
+
50
+ def find_container(self, scope: enum.IntEnum) -> "typing_extensions.Self":
51
+ container = self
52
+ while container.scope > scope and container.parent_container:
53
+ container = typing.cast("typing_extensions.Self", container.parent_container)
54
+ return container
55
+
56
+ def fetch_resolver_state(
57
+ self, resolver_id: str, is_async_resource: bool = False, is_lock_required: bool = False
58
+ ) -> ResolverState[typing.Any]:
59
+ self._check_entered()
60
+ if is_async_resource and self._is_async is False:
61
+ msg = "Resolving async resource in sync container is not allowed"
62
+ raise RuntimeError(msg)
63
+
64
+ if resolver_id not in self._resolver_states:
65
+ self._resolver_states[resolver_id] = ResolverState(is_lock_required=is_lock_required)
66
+
67
+ return self._resolver_states[resolver_id]
68
+
69
+ def override(self, resolver_id: str, override_object: object) -> None:
70
+ self._overrides[resolver_id] = override_object
71
+
72
+ def fetch_override(self, resolver_id: str) -> object | None:
73
+ return self._overrides.get(resolver_id)
74
+
75
+ def reset_override(self, resolver_id: str | None = None) -> None:
76
+ if resolver_id is None:
77
+ self._overrides = {}
78
+ else:
79
+ self._overrides.pop(resolver_id, None)
80
+
81
+ async def __aenter__(self) -> "Container":
82
+ self._is_async = True
83
+ return self
84
+
85
+ async def __aexit__(
86
+ self,
87
+ exc_type: type[BaseException] | None,
88
+ exc_val: BaseException | None,
89
+ traceback: types.TracebackType | None,
90
+ ) -> None:
91
+ self._check_entered()
92
+ for resolver_state in reversed(self._resolver_states.values()):
93
+ await resolver_state.async_tear_down()
94
+ self._exit()
95
+
96
+ def __enter__(self) -> "Container":
97
+ self._is_async = False
98
+ return self
99
+
100
+ def __exit__(
101
+ self,
102
+ exc_type: type[BaseException] | None,
103
+ exc_value: BaseException | None,
104
+ traceback: types.TracebackType | None,
105
+ ) -> None:
106
+ self._check_entered()
107
+ for resolver_state in reversed(self._resolver_states.values()):
108
+ resolver_state.sync_tear_down()
109
+ self._exit()
modern_di/graph.py ADDED
@@ -0,0 +1,39 @@
1
+ import typing
2
+
3
+ from modern_di import Container
4
+ from modern_di.resolvers import AbstractResolver, BaseCreatorResolver
5
+
6
+
7
+ if typing.TYPE_CHECKING:
8
+ import typing_extensions
9
+
10
+
11
+ T = typing.TypeVar("T")
12
+ P = typing.ParamSpec("P")
13
+
14
+
15
+ class BaseGraph:
16
+ resolvers: dict[str, AbstractResolver[typing.Any]]
17
+
18
+ def __new__(cls, *_: typing.Any, **__: typing.Any) -> "typing_extensions.Self": # noqa: ANN401
19
+ msg = f"{cls.__name__} cannot not be instantiated"
20
+ raise RuntimeError(msg)
21
+
22
+ @classmethod
23
+ def get_resolvers(cls) -> dict[str, AbstractResolver[typing.Any]]:
24
+ if not hasattr(cls, "resolvers"):
25
+ cls.resolvers = {k: v for k, v in cls.__dict__.items() if isinstance(v, AbstractResolver)}
26
+
27
+ return cls.resolvers
28
+
29
+ @classmethod
30
+ async def async_resolve_creators(cls, container: Container) -> None:
31
+ for resolver in cls.get_resolvers().values():
32
+ if isinstance(resolver, BaseCreatorResolver) and resolver.scope == container.scope:
33
+ await resolver.async_resolve(container)
34
+
35
+ @classmethod
36
+ def sync_resolve_creators(cls, container: Container) -> None:
37
+ for resolver in cls.get_resolvers().values():
38
+ if isinstance(resolver, BaseCreatorResolver) and resolver.scope == container.scope:
39
+ resolver.sync_resolve(container)
modern_di/py.typed ADDED
File without changes
@@ -0,0 +1,38 @@
1
+ import asyncio
2
+ import contextlib
3
+ import typing
4
+
5
+
6
+ T_co = typing.TypeVar("T_co", covariant=True)
7
+
8
+
9
+ class ResolverState(typing.Generic[T_co]):
10
+ __slots__ = "context_stack", "instance", "resolver_lock"
11
+
12
+ def __init__(self, is_lock_required: bool) -> None:
13
+ self.context_stack: contextlib.AsyncExitStack | contextlib.ExitStack | None = None
14
+ self.instance: T_co | None = None
15
+ self.resolver_lock: typing.Final = asyncio.Lock() if is_lock_required else None
16
+
17
+ async def async_tear_down(self) -> None:
18
+ if self.context_stack is None:
19
+ return
20
+
21
+ if isinstance(self.context_stack, contextlib.AsyncExitStack):
22
+ await self.context_stack.aclose()
23
+ else:
24
+ self.context_stack.close()
25
+ self.context_stack = None
26
+ self.instance = None
27
+
28
+ def sync_tear_down(self) -> None:
29
+ if self.context_stack is None:
30
+ return
31
+
32
+ if isinstance(self.context_stack, contextlib.AsyncExitStack):
33
+ msg = "Cannot tear down async context in `sync_tear_down`"
34
+ raise RuntimeError(msg) # noqa: TRY004
35
+
36
+ self.context_stack.close()
37
+ self.context_stack = None
38
+ self.instance = None
@@ -0,0 +1,11 @@
1
+ from modern_di.resolvers.base import AbstractResolver, BaseCreatorResolver
2
+ from modern_di.resolvers.factory import Factory
3
+ from modern_di.resolvers.resource import Resource
4
+
5
+
6
+ __all__ = [
7
+ "AbstractResolver",
8
+ "BaseCreatorResolver",
9
+ "Factory",
10
+ "Resource",
11
+ ]
@@ -0,0 +1,88 @@
1
+ import abc
2
+ import enum
3
+ import itertools
4
+ import typing
5
+ import uuid
6
+
7
+ from modern_di import Container
8
+
9
+
10
+ T_co = typing.TypeVar("T_co", covariant=True)
11
+ R = typing.TypeVar("R")
12
+ P = typing.ParamSpec("P")
13
+
14
+
15
+ class AbstractResolver(typing.Generic[T_co], abc.ABC):
16
+ BASE_SLOTS: typing.ClassVar = ["scope", "resolver_id"]
17
+
18
+ def __init__(self, scope: enum.IntEnum) -> None:
19
+ self.scope = scope
20
+ self.resolver_id: typing.Final = str(uuid.uuid4())
21
+
22
+ @abc.abstractmethod
23
+ async def async_resolve(self, container: Container) -> T_co:
24
+ """Resolve dependency asynchronously."""
25
+
26
+ @abc.abstractmethod
27
+ def sync_resolve(self, container: Container) -> T_co:
28
+ """Resolve dependency synchronously."""
29
+
30
+ def override(self, override_object: object, container: Container) -> None:
31
+ container.override(self.resolver_id, override_object)
32
+
33
+ def reset_override(self, container: Container) -> None:
34
+ container.reset_override(self.resolver_id)
35
+
36
+ @property
37
+ def cast(self) -> T_co:
38
+ return typing.cast(T_co, self)
39
+
40
+
41
+ class BaseCreatorResolver(AbstractResolver[T_co], abc.ABC):
42
+ BASE_SLOTS: typing.ClassVar = [*AbstractResolver.BASE_SLOTS, "_args", "_kwargs", "_creator"]
43
+
44
+ def __init__(
45
+ self,
46
+ scope: enum.IntEnum,
47
+ creator: typing.Callable[P, typing.Any],
48
+ *args: P.args,
49
+ **kwargs: P.kwargs,
50
+ ) -> None:
51
+ super().__init__(scope)
52
+
53
+ if any(x.scope > self.scope for x in itertools.chain(args, kwargs.values()) if isinstance(x, AbstractResolver)):
54
+ msg = "Scope of dependency cannot be more than scope of dependent"
55
+ raise RuntimeError(msg)
56
+
57
+ self._creator: typing.Final = creator
58
+ self._args: typing.Final = args
59
+ self._kwargs: typing.Final = kwargs
60
+
61
+ def _sync_build_creator(self, container: Container) -> typing.Any: # noqa: ANN401
62
+ return self._creator(
63
+ *typing.cast(
64
+ P.args, [x.sync_resolve(container) if isinstance(x, AbstractResolver) else x for x in self._args]
65
+ ),
66
+ **typing.cast(
67
+ P.kwargs,
68
+ {
69
+ k: v.sync_resolve(container) if isinstance(v, AbstractResolver) else v
70
+ for k, v in self._kwargs.items()
71
+ },
72
+ ),
73
+ )
74
+
75
+ async def _async_build_creator(self, container: Container) -> typing.Any: # noqa: ANN401
76
+ return self._creator(
77
+ *typing.cast(
78
+ P.args,
79
+ [await x.async_resolve(container) if isinstance(x, AbstractResolver) else x for x in self._args],
80
+ ),
81
+ **typing.cast(
82
+ P.kwargs,
83
+ {
84
+ k: await v.async_resolve(container) if isinstance(v, AbstractResolver) else v
85
+ for k, v in self._kwargs.items()
86
+ },
87
+ ),
88
+ )
@@ -0,0 +1,46 @@
1
+ import enum
2
+ import typing
3
+
4
+ from modern_di import Container
5
+ from modern_di.resolvers import BaseCreatorResolver
6
+
7
+
8
+ T_co = typing.TypeVar("T_co", covariant=True)
9
+ P = typing.ParamSpec("P")
10
+
11
+
12
+ class Factory(BaseCreatorResolver[T_co]):
13
+ __slots__ = [*BaseCreatorResolver.BASE_SLOTS, "_creator"]
14
+
15
+ def __init__(
16
+ self,
17
+ scope: enum.IntEnum,
18
+ creator: typing.Callable[P, T_co],
19
+ *args: P.args,
20
+ **kwargs: P.kwargs,
21
+ ) -> None:
22
+ super().__init__(scope, creator, *args, **kwargs)
23
+
24
+ async def async_resolve(self, container: Container) -> T_co:
25
+ container = container.find_container(self.scope)
26
+ if (override := container.fetch_override(self.resolver_id)) is not None:
27
+ return typing.cast(T_co, override)
28
+
29
+ resolver_state = container.fetch_resolver_state(self.resolver_id)
30
+ if resolver_state.instance is not None:
31
+ return typing.cast(T_co, resolver_state.instance)
32
+
33
+ resolver_state.instance = typing.cast(T_co, await self._async_build_creator(container))
34
+ return resolver_state.instance
35
+
36
+ def sync_resolve(self, container: Container) -> T_co:
37
+ container = container.find_container(self.scope)
38
+ if (override := container.fetch_override(self.resolver_id)) is not None:
39
+ return typing.cast(T_co, override)
40
+
41
+ resolver_state = container.fetch_resolver_state(self.resolver_id)
42
+ if resolver_state.instance is not None:
43
+ return typing.cast(T_co, resolver_state.instance)
44
+
45
+ resolver_state.instance = self._sync_build_creator(container)
46
+ return typing.cast(T_co, resolver_state.instance)
@@ -0,0 +1,95 @@
1
+ import contextlib
2
+ import enum
3
+ import inspect
4
+ import typing
5
+
6
+ from modern_di import Container
7
+ from modern_di.resolvers import BaseCreatorResolver
8
+
9
+
10
+ T_co = typing.TypeVar("T_co", covariant=True)
11
+ P = typing.ParamSpec("P")
12
+
13
+
14
+ class Resource(BaseCreatorResolver[T_co]):
15
+ __slots__ = [*BaseCreatorResolver.BASE_SLOTS, "_creator", "_args", "_kwargs", "_is_async"]
16
+
17
+ def _is_creator_async(
18
+ self,
19
+ _: contextlib.AbstractContextManager[T_co] | contextlib.AbstractAsyncContextManager[T_co],
20
+ ) -> typing.TypeGuard[contextlib.AbstractAsyncContextManager[T_co]]:
21
+ return self._is_async
22
+
23
+ def __init__(
24
+ self,
25
+ scope: enum.IntEnum,
26
+ creator: typing.Callable[P, typing.Iterator[T_co] | typing.AsyncIterator[T_co]],
27
+ *args: P.args,
28
+ **kwargs: P.kwargs,
29
+ ) -> None:
30
+ new_creator: typing.Any
31
+ if inspect.isasyncgenfunction(creator):
32
+ self._is_async = True
33
+ new_creator = contextlib.asynccontextmanager(creator)
34
+ elif inspect.isgeneratorfunction(creator):
35
+ self._is_async = False
36
+ new_creator = contextlib.contextmanager(creator)
37
+ else:
38
+ msg = "Unsupported resource type"
39
+ raise RuntimeError(msg)
40
+
41
+ super().__init__(scope, new_creator, *args, **kwargs)
42
+
43
+ async def async_resolve(self, container: Container) -> T_co:
44
+ container = container.find_container(self.scope)
45
+ if (override := container.fetch_override(self.resolver_id)) is not None:
46
+ return typing.cast(T_co, override)
47
+
48
+ resolver_state = container.fetch_resolver_state(
49
+ self.resolver_id, is_async_resource=self._is_async, is_lock_required=self._is_async
50
+ )
51
+ if resolver_state.instance is not None:
52
+ return typing.cast(T_co, resolver_state.instance)
53
+
54
+ if resolver_state.resolver_lock:
55
+ await resolver_state.resolver_lock.acquire()
56
+
57
+ try:
58
+ if resolver_state.instance is not None:
59
+ return typing.cast(T_co, resolver_state.instance)
60
+
61
+ _intermediate_ = await self._async_build_creator(container)
62
+
63
+ if self._is_creator_async(self._creator): # type: ignore[arg-type]
64
+ resolver_state.context_stack = contextlib.AsyncExitStack()
65
+ resolver_state.instance = await resolver_state.context_stack.enter_async_context(_intermediate_)
66
+ else:
67
+ resolver_state.context_stack = contextlib.ExitStack()
68
+ resolver_state.instance = resolver_state.context_stack.enter_context(_intermediate_)
69
+ finally:
70
+ if resolver_state.resolver_lock:
71
+ resolver_state.resolver_lock.release()
72
+
73
+ return typing.cast(T_co, resolver_state.instance)
74
+
75
+ def sync_resolve(self, container: Container) -> T_co:
76
+ container = container.find_container(self.scope)
77
+ if (override := container.fetch_override(self.resolver_id)) is not None:
78
+ return typing.cast(T_co, override)
79
+
80
+ resolver_state = container.fetch_resolver_state(self.resolver_id)
81
+ if resolver_state.instance is not None:
82
+ return typing.cast(T_co, resolver_state.instance)
83
+
84
+ if self._is_async:
85
+ msg = "Async resource cannot be resolved synchronously"
86
+ raise RuntimeError(msg)
87
+
88
+ _intermediate_ = self._sync_build_creator(container)
89
+
90
+ resolver_state.context_stack = contextlib.ExitStack()
91
+ resolver_state.instance = resolver_state.context_stack.enter_context(
92
+ typing.cast(contextlib.AbstractContextManager[typing.Any], _intermediate_)
93
+ )
94
+
95
+ return typing.cast(T_co, resolver_state.instance)
modern_di/scope.py ADDED
@@ -0,0 +1,8 @@
1
+ import enum
2
+
3
+
4
+ class Scope(enum.IntEnum):
5
+ APP = 1
6
+ REQUEST = 2
7
+ ACTION = 3
8
+ STEP = 4
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.3
2
+ Name: modern-di
3
+ Version: 0.1.0
4
+ Summary: Simple Dependency Injection framework
5
+ Project-URL: repository, https://github.com/modern-python/that-depends
6
+ Project-URL: docs, https://that-depends.readthedocs.io
7
+ Author-email: Artur Shiriev <me@shiriev.ru>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: dependency injector,di,ioc-container,mocks,python
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: <4,>=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ "Modern-DI"
21
+ ==
22
+ [![Test Coverage](https://codecov.io/gh/modern-python/modern-di/branch/main/graph/badge.svg)](https://codecov.io/gh/modern-python/modern-di)
23
+ [![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
24
+ [![Supported versions](https://img.shields.io/pypi/pyversions/modern-di.svg)](https://pypi.python.org/pypi/modern-di)
25
+ [![downloads](https://img.shields.io/pypi/dm/modern-di.svg)](https://pypistats.org/packages/modern-di)
26
+ [![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di)](https://github.com/modern-python/modern-di/stargazers)
27
+
28
+ Dependency injection framework for Python inspired by `dependency-injector` and `dishka`.
29
+
30
+ It is production-ready and gives you the following:
31
+ - Simple DI framework with IOC-container.
32
+ - Async and sync resolving.
33
+ - Python 3.10-3.13 support.
34
+ - Full coverage by types annotations (mypy in strict mode).
35
+ - Overriding dependencies for tests.
36
+ - Package with zero dependencies.
37
+
38
+ 📚 [Documentation](https://modern-di.readthedocs.io)
@@ -0,0 +1,14 @@
1
+ modern_di/__init__.py,sha256=2xhgfqi4gpzPrlfam5HSwTFRroWmYIybDgsSYD9pPNg,194
2
+ modern_di/container.py,sha256=o-6_eoSGpOrIZlAXDryISWmgnSLq7u0YAVRXHvkWU0s,3742
3
+ modern_di/graph.py,sha256=V6LUbtcZb64jBD-GJB2bJijEcPBHSYEEI9WZGflrFpQ,1335
4
+ modern_di/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ modern_di/resolver_state.py,sha256=rRIoyWPK7XtMeWPBFuEe8t81WdrHbj5-jtpOqZWf0jY,1206
6
+ modern_di/scope.py,sha256=nnrTLnFT_dMDLVIwvJHcGlJsB2k1WIHusrRSGQIMEsg,97
7
+ modern_di/resolvers/__init__.py,sha256=ubCeXD-44otfS1-AG5vRbTaqn8d6szY7cKQTprFT3Pk,271
8
+ modern_di/resolvers/base.py,sha256=BMZbYfkIzAW8-8CQ9aRVIu5D8zw9owKv4nz-z0EMYmg,2897
9
+ modern_di/resolvers/factory.py,sha256=CyMvl--t3bCapxARFnCKNJNgy1svCR5c9HcwHm85G8Q,1650
10
+ modern_di/resolvers/resource.py,sha256=s0c2PQQSjffdKKXxZ6fJVKJaiqqBgQLZ8mTpzKjDb4Q,3688
11
+ modern_di-0.1.0.dist-info/METADATA,sha256=t_wifprG9bqMg-H6l8kTRoWYFPZcbJm6bMtBjsaJLeM,1825
12
+ modern_di-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
13
+ modern_di-0.1.0.dist-info/licenses/LICENSE,sha256=aA5_eJwDKqnGvL7PbkPb9x5f-VGIqZ9cvWWkeGqeD90,1070
14
+ modern_di-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 modern-python
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.