python-injection 0.14.5__tar.gz → 0.14.6__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.
Files changed (26) hide show
  1. {python_injection-0.14.5 → python_injection-0.14.6}/PKG-INFO +1 -1
  2. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/asynchronous.py +13 -1
  3. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/injectables.py +57 -43
  4. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/module.py +2 -1
  5. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/scope.py +8 -3
  6. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/slots.py +3 -3
  7. {python_injection-0.14.5 → python_injection-0.14.6}/pyproject.toml +2 -1
  8. {python_injection-0.14.5 → python_injection-0.14.6}/.gitignore +0 -0
  9. {python_injection-0.14.5 → python_injection-0.14.6}/README.md +0 -0
  10. {python_injection-0.14.5 → python_injection-0.14.6}/injection/__init__.py +0 -0
  11. {python_injection-0.14.5 → python_injection-0.14.6}/injection/__init__.pyi +0 -0
  12. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/__init__.py +0 -0
  13. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/__init__.py +0 -0
  14. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/event.py +0 -0
  15. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/invertible.py +0 -0
  16. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/key.py +0 -0
  17. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/lazy.py +0 -0
  18. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/common/type.py +0 -0
  19. {python_injection-0.14.5 → python_injection-0.14.6}/injection/_core/descriptors.py +0 -0
  20. {python_injection-0.14.5 → python_injection-0.14.6}/injection/exceptions.py +0 -0
  21. {python_injection-0.14.5 → python_injection-0.14.6}/injection/integrations/__init__.py +0 -0
  22. {python_injection-0.14.5 → python_injection-0.14.6}/injection/integrations/fastapi.py +0 -0
  23. {python_injection-0.14.5 → python_injection-0.14.6}/injection/py.typed +0 -0
  24. {python_injection-0.14.5 → python_injection-0.14.6}/injection/testing/__init__.py +0 -0
  25. {python_injection-0.14.5 → python_injection-0.14.6}/injection/testing/__init__.pyi +0 -0
  26. {python_injection-0.14.5 → python_injection-0.14.6}/injection/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.14.5
3
+ Version: 0.14.6
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -1,7 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import Awaitable, Callable, Generator
3
3
  from dataclasses import dataclass
4
- from typing import Any, NoReturn, Protocol, runtime_checkable
4
+ from typing import Any, AsyncContextManager, NoReturn, Protocol, runtime_checkable
5
5
 
6
6
 
7
7
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -45,3 +45,15 @@ class SyncCaller[**P, T](Caller[P, T]):
45
45
 
46
46
  def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
47
47
  return self.callable(*args, **kwargs)
48
+
49
+
50
+ try:
51
+ import anyio
52
+
53
+ def create_semaphore(value: int) -> AsyncContextManager[Any]:
54
+ return anyio.Semaphore(value)
55
+ except ImportError: # pragma: no cover
56
+ import asyncio
57
+
58
+ def create_semaphore(value: int) -> AsyncContextManager[Any]:
59
+ return asyncio.Semaphore(value)
@@ -1,7 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import MutableMapping
2
+ from collections.abc import Awaitable, Callable, MutableMapping
3
3
  from contextlib import suppress
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
+ from functools import partial
5
6
  from typing import (
6
7
  Any,
7
8
  AsyncContextManager,
@@ -12,7 +13,7 @@ from typing import (
12
13
  runtime_checkable,
13
14
  )
14
15
 
15
- from injection._core.common.asynchronous import Caller
16
+ from injection._core.common.asynchronous import Caller, create_semaphore
16
17
  from injection._core.scope import Scope, get_active_scopes, get_scope
17
18
  from injection.exceptions import InjectionError
18
19
 
@@ -37,12 +38,12 @@ class Injectable[T](Protocol):
37
38
  raise NotImplementedError
38
39
 
39
40
 
40
- @dataclass(repr=False, frozen=True, slots=True)
41
- class BaseInjectable[T](Injectable[T], ABC):
42
- factory: Caller[..., T]
41
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
42
+ class BaseInjectable[R, T](Injectable[T], ABC):
43
+ factory: Caller[..., R]
43
44
 
44
45
 
45
- class SimpleInjectable[T](BaseInjectable[T]):
46
+ class SimpleInjectable[T](BaseInjectable[T, T]):
46
47
  __slots__ = ()
47
48
 
48
49
  async def aget_instance(self) -> T:
@@ -52,7 +53,44 @@ class SimpleInjectable[T](BaseInjectable[T]):
52
53
  return self.factory.call()
53
54
 
54
55
 
55
- class SingletonInjectable[T](BaseInjectable[T]):
56
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
57
+ class CachedInjectable[R, T](BaseInjectable[R, T], ABC):
58
+ __semaphore: AsyncContextManager[Any] = field(
59
+ default_factory=partial(create_semaphore, 1),
60
+ init=False,
61
+ hash=False,
62
+ )
63
+
64
+ async def aget_or_create[K](
65
+ self,
66
+ cache: MutableMapping[K, T],
67
+ key: K,
68
+ factory: Callable[..., Awaitable[T]],
69
+ ) -> T:
70
+ async with self.__semaphore:
71
+ with suppress(KeyError):
72
+ return cache[key]
73
+
74
+ instance = await factory()
75
+ cache[key] = instance
76
+
77
+ return instance
78
+
79
+ def get_or_create[K](
80
+ self,
81
+ cache: MutableMapping[K, T],
82
+ key: K,
83
+ factory: Callable[..., T],
84
+ ) -> T:
85
+ with suppress(KeyError):
86
+ return cache[key]
87
+
88
+ instance = factory()
89
+ cache[key] = instance
90
+ return instance
91
+
92
+
93
+ class SingletonInjectable[T](CachedInjectable[T, T]):
56
94
  __slots__ = ("__dict__",)
57
95
 
58
96
  __key: ClassVar[str] = "$instance"
@@ -66,32 +104,17 @@ class SingletonInjectable[T](BaseInjectable[T]):
66
104
  return self.__dict__
67
105
 
68
106
  async def aget_instance(self) -> T:
69
- cache = self.__cache
70
-
71
- with suppress(KeyError):
72
- return cache[self.__key]
73
-
74
- instance = await self.factory.acall()
75
- cache[self.__key] = instance
76
- return instance
107
+ return await self.aget_or_create(self.__cache, self.__key, self.factory.acall)
77
108
 
78
109
  def get_instance(self) -> T:
79
- cache = self.__cache
80
-
81
- with suppress(KeyError):
82
- return cache[self.__key]
83
-
84
- instance = self.factory.call()
85
- cache[self.__key] = instance
86
- return instance
110
+ return self.get_or_create(self.__cache, self.__key, self.factory.call)
87
111
 
88
112
  def unlock(self) -> None:
89
113
  self.__cache.pop(self.__key, None)
90
114
 
91
115
 
92
116
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
93
- class ScopedInjectable[R, T](Injectable[T], ABC):
94
- factory: Caller[..., R]
117
+ class ScopedInjectable[R, T](CachedInjectable[R, T], ABC):
95
118
  scope_name: str
96
119
 
97
120
  @property
@@ -108,29 +131,20 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
108
131
 
109
132
  async def aget_instance(self) -> T:
110
133
  scope = self.get_scope()
111
-
112
- with suppress(KeyError):
113
- return scope.cache[self]
114
-
115
- instance = await self.abuild(scope)
116
- self.set_instance(instance, scope)
117
- return instance
134
+ factory = partial(self.abuild, scope)
135
+ return await self.aget_or_create(scope.cache, self, factory)
118
136
 
119
137
  def get_instance(self) -> T:
120
138
  scope = self.get_scope()
121
-
122
- with suppress(KeyError):
123
- return scope.cache[self]
124
-
125
- instance = self.build(scope)
126
- self.set_instance(instance, scope)
127
- return instance
139
+ factory = partial(self.build, scope)
140
+ return self.get_or_create(scope.cache, self, factory)
128
141
 
129
142
  def get_scope(self) -> Scope:
130
143
  return get_scope(self.scope_name)
131
144
 
132
- def set_instance(self, instance: T, scope: Scope) -> None:
133
- scope.cache[self] = instance
145
+ def setdefault(self, instance: T) -> T:
146
+ scope = self.get_scope()
147
+ return self.get_or_create(scope.cache, self, lambda: instance)
134
148
 
135
149
  def unlock(self) -> None:
136
150
  if self.is_locked:
@@ -174,7 +188,7 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
174
188
  scope.cache.pop(self, None)
175
189
 
176
190
 
177
- @dataclass(repr=False, frozen=True, slots=True)
191
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
178
192
  class ShouldBeInjectable[T](Injectable[T]):
179
193
  cls: type[T]
180
194
 
@@ -300,7 +300,8 @@ class Locator(Broker):
300
300
  if injectable.is_locked:
301
301
  continue
302
302
 
303
- await injectable.aget_instance()
303
+ with suppress(SkipInjectable):
304
+ await injectable.aget_instance()
304
305
 
305
306
  def add_listener(self, listener: EventListener) -> Self:
306
307
  self.__channel.add_listener(listener)
@@ -98,13 +98,18 @@ def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
98
98
 
99
99
 
100
100
  def get_active_scopes(name: str) -> tuple[Scope, ...]:
101
- return tuple(__SCOPES[name].active_scopes)
101
+ state = __SCOPES.get(name)
102
+
103
+ if state is None:
104
+ return ()
105
+
106
+ return tuple(state.active_scopes)
102
107
 
103
108
 
104
109
  def get_scope(name: str) -> Scope:
105
- scope = __SCOPES[name].get_scope()
110
+ state = __SCOPES.get(name)
106
111
 
107
- if scope is None:
112
+ if state is None or (scope := state.get_scope()) is None:
108
113
  raise ScopeUndefinedError(
109
114
  f"Scope `{name}` isn't defined in the current context."
110
115
  )
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Any, Protocol, runtime_checkable
4
4
 
5
5
  from injection._core.injectables import ScopedInjectable
6
+ from injection.exceptions import InjectionError
6
7
 
7
8
 
8
9
  @runtime_checkable
@@ -19,6 +20,5 @@ class ScopedSlot[T](Slot[T]):
19
20
  injectable: ScopedInjectable[Any, T]
20
21
 
21
22
  def set(self, instance: T, /) -> None:
22
- injectable = self.injectable
23
- scope = injectable.get_scope()
24
- injectable.set_instance(instance, scope)
23
+ if self.injectable.setdefault(instance) is not instance:
24
+ raise InjectionError("Slot already set.")
@@ -9,6 +9,7 @@ bench = [
9
9
  "types-tabulate",
10
10
  ]
11
11
  dev = [
12
+ "anyio",
12
13
  "hatch",
13
14
  "mypy",
14
15
  "ruff",
@@ -24,7 +25,7 @@ test = [
24
25
 
25
26
  [project]
26
27
  name = "python-injection"
27
- version = "0.14.5"
28
+ version = "0.14.6"
28
29
  description = "Fast and easy dependency injection framework."
29
30
  license = { text = "MIT" }
30
31
  readme = "README.md"