python-injection 0.26.2__tar.gz → 0.26.4__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 (32) hide show
  1. {python_injection-0.26.2 → python_injection-0.26.4}/PKG-INFO +9 -4
  2. {python_injection-0.26.2 → python_injection-0.26.4}/docs/index.md +6 -2
  3. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/asynchronous.py +5 -10
  4. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/event.py +3 -3
  5. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/lazy.py +1 -1
  6. python_injection-0.26.4/injection/_core/common/threading.py +22 -0
  7. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/descriptors.py +2 -2
  8. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/injectables.py +8 -10
  9. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/locator.py +2 -3
  10. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/module.py +41 -34
  11. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/scope.py +29 -19
  12. {python_injection-0.26.2 → python_injection-0.26.4}/injection/entrypoint.py +10 -10
  13. {python_injection-0.26.2 → python_injection-0.26.4}/pyproject.toml +5 -4
  14. python_injection-0.26.2/injection/_core/common/threading.py +0 -13
  15. {python_injection-0.26.2 → python_injection-0.26.4}/.gitignore +0 -0
  16. {python_injection-0.26.2 → python_injection-0.26.4}/LICENSE +0 -0
  17. {python_injection-0.26.2 → python_injection-0.26.4}/injection/__init__.py +0 -0
  18. {python_injection-0.26.2 → python_injection-0.26.4}/injection/__init__.pyi +0 -0
  19. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/__init__.py +0 -0
  20. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/asfunction.py +0 -0
  21. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/__init__.py +0 -0
  22. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/invertible.py +0 -0
  23. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/type.py +0 -0
  24. {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/slots.py +0 -0
  25. {python_injection-0.26.2 → python_injection-0.26.4}/injection/exceptions.py +0 -0
  26. {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/__init__.py +0 -0
  27. {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/fastapi.py +0 -0
  28. {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/fastapi.pyi +0 -0
  29. {python_injection-0.26.2 → python_injection-0.26.4}/injection/loaders.py +0 -0
  30. {python_injection-0.26.2 → python_injection-0.26.4}/injection/py.typed +0 -0
  31. {python_injection-0.26.2 → python_injection-0.26.4}/injection/testing/__init__.py +0 -0
  32. {python_injection-0.26.2 → python_injection-0.26.4}/injection/testing/__init__.pyi +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-injection
3
- Version: 0.26.2
3
+ Version: 0.26.4
4
4
  Summary: Dead-simple dependency injection framework for Python.
5
5
  Project-URL: Documentation, https://python-injection.remimd.dev
6
6
  Project-URL: Repository, https://github.com/100nm/python-injection
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
21
22
  Classifier: Topic :: Software Development :: Libraries
22
23
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -31,8 +32,8 @@ Description-Content-Type: text/markdown
31
32
 
32
33
  # python-injection
33
34
 
34
- [![PyPI - Version](https://img.shields.io/pypi/v/python-injection.svg?color=546d78&style=for-the-badge)](https://pypi.org/project/python-injection)
35
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-injection.svg?color=546d78&style=for-the-badge)](https://pypistats.org/packages/python-injection)
35
+ [![PyPI - Version](https://shieldcn.dev/pypi/v/python-injection.svg?variant=secondary)](https://pypi.org/project/python-injection)
36
+ [![PyPI - Downloads](https://shieldcn.dev/pypi/dm/python-injection.svg?variant=secondary)](https://pypistats.org/packages/python-injection)
36
37
 
37
38
  ## Project motivations
38
39
 
@@ -68,6 +69,7 @@ Simply apply the decorators and the package takes care of the rest.
68
69
  ```python
69
70
  from injection import injectable, inject, singleton
70
71
 
72
+
71
73
  @singleton
72
74
  class Printer:
73
75
  def __init__(self):
@@ -77,6 +79,7 @@ class Printer:
77
79
  self.history.append(message)
78
80
  print(message)
79
81
 
82
+
80
83
  @injectable
81
84
  class Service:
82
85
  def __init__(self, printer: Printer):
@@ -85,10 +88,12 @@ class Service:
85
88
  def hello(self):
86
89
  self.printer.print("Hello world!")
87
90
 
91
+
88
92
  @inject
89
93
  def main(service: Service):
90
94
  service.hello()
91
95
 
96
+
92
97
  if __name__ == "__main__":
93
98
  main()
94
99
  ```
@@ -1,7 +1,7 @@
1
1
  # python-injection
2
2
 
3
- [![PyPI - Version](https://img.shields.io/pypi/v/python-injection.svg?color=546d78&style=for-the-badge)](https://pypi.org/project/python-injection)
4
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-injection.svg?color=546d78&style=for-the-badge)](https://pypistats.org/packages/python-injection)
3
+ [![PyPI - Version](https://shieldcn.dev/pypi/v/python-injection.svg?variant=secondary)](https://pypi.org/project/python-injection)
4
+ [![PyPI - Downloads](https://shieldcn.dev/pypi/dm/python-injection.svg?variant=secondary)](https://pypistats.org/packages/python-injection)
5
5
 
6
6
  ## Project motivations
7
7
 
@@ -37,6 +37,7 @@ Simply apply the decorators and the package takes care of the rest.
37
37
  ```python
38
38
  from injection import injectable, inject, singleton
39
39
 
40
+
40
41
  @singleton
41
42
  class Printer:
42
43
  def __init__(self):
@@ -46,6 +47,7 @@ class Printer:
46
47
  self.history.append(message)
47
48
  print(message)
48
49
 
50
+
49
51
  @injectable
50
52
  class Service:
51
53
  def __init__(self, printer: Printer):
@@ -54,10 +56,12 @@ class Service:
54
56
  def hello(self):
55
57
  self.printer.print("Hello world!")
56
58
 
59
+
57
60
  @inject
58
61
  def main(service: Service):
59
62
  service.hello()
60
63
 
64
+
61
65
  if __name__ == "__main__":
62
66
  main()
63
67
  ```
@@ -1,15 +1,10 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import Awaitable, Callable, Generator
3
+ from contextlib import AbstractAsyncContextManager
3
4
  from dataclasses import dataclass
4
- from typing import (
5
- Any,
6
- AsyncContextManager,
7
- NoReturn,
8
- Protocol,
9
- runtime_checkable,
10
- )
5
+ from typing import Any, NoReturn, Protocol, runtime_checkable
11
6
 
12
- AsyncSemaphore: Callable[[int], AsyncContextManager[Any]]
7
+ AsyncLock: Callable[[], AbstractAsyncContextManager[Any]]
13
8
 
14
9
  try:
15
10
  import anyio
@@ -17,10 +12,10 @@ try:
17
12
  except ImportError: # pragma: no cover
18
13
  import asyncio
19
14
 
20
- AsyncSemaphore = asyncio.Semaphore
15
+ AsyncLock = asyncio.Lock
21
16
 
22
17
  else:
23
- AsyncSemaphore = anyio.Semaphore
18
+ AsyncLock = anyio.Lock
24
19
 
25
20
 
26
21
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -1,8 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Iterator
3
- from contextlib import ExitStack, contextmanager
3
+ from contextlib import AbstractContextManager, ExitStack, contextmanager
4
4
  from dataclasses import dataclass, field
5
- from typing import ContextManager, Self
5
+ from typing import Self
6
6
  from weakref import WeakSet
7
7
 
8
8
 
@@ -14,7 +14,7 @@ class EventListener(ABC):
14
14
  __slots__ = ("__weakref__",)
15
15
 
16
16
  @abstractmethod
17
- def on_event(self, event: Event, /) -> ContextManager[None] | None:
17
+ def on_event(self, event: Event, /) -> AbstractContextManager[None] | None:
18
18
  raise NotImplementedError
19
19
 
20
20
 
@@ -19,7 +19,7 @@ class Lazy[T](Invertible[T]):
19
19
  @property
20
20
  def is_set(self) -> bool:
21
21
  try:
22
- self.__value
22
+ self.__value # noqa: B018
23
23
  except AttributeError:
24
24
  return False
25
25
 
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import AbstractContextManager, nullcontext
4
+ from os import getenv
5
+ from threading import Lock
6
+ from typing import Any, Final
7
+
8
+ _PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
9
+ int(getenv("PYTHON_INJECTION_THREADSAFE", "0"))
10
+ )
11
+
12
+
13
+ def lock_if_threadsafe(
14
+ lock: AbstractContextManager[Any] | None = None,
15
+ threadsafe: bool | None = None,
16
+ ) -> AbstractContextManager[Any]:
17
+ threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
18
+
19
+ if not threadsafe:
20
+ return nullcontext()
21
+
22
+ return lock or Lock()
@@ -73,7 +73,7 @@ class BoundMappedScope:
73
73
  async def adefine(
74
74
  self,
75
75
  /,
76
- kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
76
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
77
77
  threadsafe: bool | None = None,
78
78
  ) -> AsyncIterator[None]:
79
79
  async with adefine_scope(self.name, kind, threadsafe) as scope:
@@ -86,7 +86,7 @@ class BoundMappedScope:
86
86
  def define(
87
87
  self,
88
88
  /,
89
- kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
89
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
90
90
  threadsafe: bool | None = None,
91
91
  ) -> Iterator[None]:
92
92
  with define_scope(self.name, kind, threadsafe) as scope:
@@ -1,20 +1,18 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Awaitable, Callable, MutableMapping, Sequence
3
- from contextlib import suppress
3
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager, suppress
4
4
  from dataclasses import dataclass, field
5
5
  from functools import partial
6
6
  from typing import (
7
7
  Any,
8
- AsyncContextManager,
9
8
  ClassVar,
10
- ContextManager,
11
9
  NoReturn,
12
10
  Protocol,
13
11
  Self,
14
12
  runtime_checkable,
15
13
  )
16
14
 
17
- from injection._core.common.asynchronous import AsyncSemaphore, Caller
15
+ from injection._core.common.asynchronous import AsyncLock, Caller
18
16
  from injection._core.common.type import InputType
19
17
  from injection._core.scope import Scope, get_first_scope, in_scope_cache
20
18
  from injection._core.slots import SlotKey
@@ -53,12 +51,12 @@ class TransientInjectable[T](Injectable[T]):
53
51
 
54
52
 
55
53
  class CacheLogic[T]:
56
- __slots__ = ("__semaphore",)
54
+ __slots__ = ("__lock",)
57
55
 
58
- __semaphore: AsyncContextManager[Any]
56
+ __lock: AbstractAsyncContextManager[Any]
59
57
 
60
58
  def __init__(self) -> None:
61
- self.__semaphore = AsyncSemaphore(1)
59
+ self.__lock = AsyncLock()
62
60
 
63
61
  async def aget_or_create[K](
64
62
  self,
@@ -66,7 +64,7 @@ class CacheLogic[T]:
66
64
  key: K,
67
65
  factory: Callable[..., Awaitable[T]],
68
66
  ) -> T:
69
- async with self.__semaphore:
67
+ async with self.__lock:
70
68
  with suppress(KeyError):
71
69
  return cache[key]
72
70
 
@@ -169,7 +167,7 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
169
167
  return partial(cls, scope_names=names)
170
168
 
171
169
 
172
- class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
170
+ class AsyncCMScopedInjectable[T](ScopedInjectable[AbstractAsyncContextManager[T], T]):
173
171
  __slots__ = ()
174
172
 
175
173
  async def abuild(self, scope: Scope) -> T:
@@ -180,7 +178,7 @@ class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
180
178
  raise RuntimeError("Can't use async context manager synchronously.")
181
179
 
182
180
 
183
- class CMScopedInjectable[T](ScopedInjectable[ContextManager[T], T]):
181
+ class CMScopedInjectable[T](ScopedInjectable[AbstractContextManager[T], T]):
184
182
  __slots__ = ()
185
183
 
186
184
  async def abuild(self, scope: Scope) -> T:
@@ -2,13 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from collections.abc import Awaitable, Callable, Collection, Iterable, Iterator
5
- from contextlib import suppress
5
+ from contextlib import AbstractContextManager, suppress
6
6
  from dataclasses import dataclass, field
7
7
  from enum import StrEnum
8
8
  from inspect import iscoroutinefunction
9
9
  from typing import (
10
10
  Any,
11
- ContextManager,
12
11
  Literal,
13
12
  NamedTuple,
14
13
  Protocol,
@@ -226,7 +225,7 @@ class Locator:
226
225
  self.__channel.add_listener(listener)
227
226
  return self
228
227
 
229
- def dispatch(self, event: Event) -> ContextManager[None]:
228
+ def dispatch(self, event: Event) -> AbstractContextManager[None]:
230
229
  return self.__channel.dispatch(event)
231
230
 
232
231
  def __iter_injectables(
@@ -14,7 +14,13 @@ from collections.abc import (
14
14
  Iterator,
15
15
  Mapping,
16
16
  )
17
- from contextlib import asynccontextmanager, contextmanager, suppress
17
+ from contextlib import (
18
+ AbstractAsyncContextManager,
19
+ AbstractContextManager,
20
+ asynccontextmanager,
21
+ contextmanager,
22
+ suppress,
23
+ )
18
24
  from dataclasses import dataclass, field
19
25
  from enum import StrEnum
20
26
  from functools import partialmethod, singledispatchmethod, update_wrapper
@@ -29,18 +35,9 @@ from inspect import (
29
35
  )
30
36
  from inspect import signature as inspect_signature
31
37
  from logging import Logger, getLogger
38
+ from threading import RLock
32
39
  from types import MethodType
33
- from typing import (
34
- TYPE_CHECKING,
35
- Any,
36
- AsyncContextManager,
37
- ClassVar,
38
- ContextManager,
39
- Literal,
40
- NamedTuple,
41
- Self,
42
- overload,
43
- )
40
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, overload
44
41
 
45
42
  from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types
46
43
 
@@ -48,7 +45,7 @@ from injection._core.common.asynchronous import Caller, SimpleAwaitable
48
45
  from injection._core.common.event import Event, EventChannel, EventListener
49
46
  from injection._core.common.invertible import Invertible, SimpleInvertible
50
47
  from injection._core.common.lazy import Lazy
51
- from injection._core.common.threading import get_lock
48
+ from injection._core.common.threading import lock_if_threadsafe
52
49
  from injection._core.common.type import (
53
50
  InputType,
54
51
  TypeInfo,
@@ -157,7 +154,7 @@ class Priority(StrEnum):
157
154
  type PriorityStr = Literal["low", "high"]
158
155
 
159
156
  type ContextManagerRecipe[**P, T] = (
160
- Callable[P, ContextManager[T]] | Callable[P, AsyncContextManager[T]]
157
+ Callable[P, AbstractContextManager[T]] | Callable[P, AbstractAsyncContextManager[T]]
161
158
  )
162
159
  type GeneratorRecipe[**P, T] = (
163
160
  Callable[P, Generator[T, Any, Any]] | Callable[P, AsyncGenerator[T, Any]]
@@ -184,6 +181,11 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
184
181
  init=False,
185
182
  repr=False,
186
183
  )
184
+ __lock: RLock = field(
185
+ default_factory=RLock,
186
+ init=False,
187
+ repr=False,
188
+ )
187
189
  __loggers: list[Logger] = field(
188
190
  default_factory=lambda: [getLogger("python-injection")],
189
191
  init=False,
@@ -231,7 +233,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
231
233
  ignore_type_hint: bool = False,
232
234
  inject: bool = True,
233
235
  on: TypeInfo[T] = (),
234
- mode: Mode | ModeStr = Mode.get_default(),
236
+ mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
235
237
  ) -> Any:
236
238
  def decorator(wp: Recipe[P, T]) -> Recipe[P, T]:
237
239
  hints = on if ignore_type_hint else (wp, on)
@@ -255,7 +257,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
255
257
  ignore_type_hint: bool = False,
256
258
  inject: bool = True,
257
259
  on: TypeInfo[T] = (),
258
- mode: Mode | ModeStr = Mode.get_default(),
260
+ mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
259
261
  ) -> Any:
260
262
  def decorator(
261
263
  wrapped: Recipe[P, T] | GeneratorRecipe[P, T],
@@ -307,7 +309,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
307
309
  on: TypeInfo[T] = (),
308
310
  *,
309
311
  alias: bool = False,
310
- mode: Mode | ModeStr = Mode.get_default(),
312
+ mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
311
313
  ) -> T:
312
314
  if not alias:
313
315
  on = (type(instance), on)
@@ -325,7 +327,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
325
327
  /,
326
328
  scope_name: str,
327
329
  *,
328
- mode: Mode | ModeStr = Mode.get_default(),
330
+ mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
329
331
  ) -> SlotKey[T]:
330
332
  injectable = ScopedSlotInjectable(cls, scope_name)
331
333
  broker = StaticInjectableBroker(injectable)
@@ -341,8 +343,8 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
341
343
  ) -> Any:
342
344
  def decorator(wp: Callable[P, T]) -> Callable[P, T]:
343
345
  if isclass(wp):
344
- wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe)
345
- return wp
346
+ wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe) # type: ignore[method-assign]
347
+ return wp # type: ignore[return-value]
346
348
 
347
349
  return self.make_injected_function(wp, threadsafe)
348
350
 
@@ -354,7 +356,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
354
356
  /,
355
357
  threadsafe: bool | None = None,
356
358
  ) -> InjectMetadata[P, T]:
357
- return InjectMetadata(wrapped, threadsafe).listen(self)
359
+ return InjectMetadata(wrapped, self.__lock, threadsafe).listen(self)
358
360
 
359
361
  if TYPE_CHECKING: # pragma: no cover
360
362
 
@@ -401,7 +403,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
401
403
  *,
402
404
  threadsafe: bool | None = None,
403
405
  ) -> T:
404
- with get_lock(threadsafe):
406
+ with lock_if_threadsafe(self.__lock, threadsafe):
405
407
  injectable = self[cls]
406
408
  return await injectable.aget_instance()
407
409
 
@@ -411,7 +413,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
411
413
  *,
412
414
  threadsafe: bool | None = None,
413
415
  ) -> T:
414
- with get_lock(threadsafe):
416
+ with lock_if_threadsafe(self.__lock, threadsafe):
415
417
  injectable = self[cls]
416
418
  return injectable.get_instance()
417
419
 
@@ -583,7 +585,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
583
585
  self,
584
586
  module: Module,
585
587
  *,
586
- priority: Priority | PriorityStr = Priority.get_default(),
588
+ priority: Priority | PriorityStr = Priority.get_default(), # noqa: B008
587
589
  ) -> Self:
588
590
  if module is self:
589
591
  raise ModuleError("Module can't be used by itself.")
@@ -602,9 +604,9 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
602
604
  return self
603
605
 
604
606
  def stop_using(self, module: Module) -> Self:
605
- event = ModuleRemoved(self, module)
607
+ if module in self.__modules:
608
+ event = ModuleRemoved(self, module)
606
609
 
607
- with suppress(KeyError):
608
610
  with self.dispatch(event):
609
611
  self.__modules.pop(module)
610
612
  module.remove_listener(self)
@@ -616,7 +618,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
616
618
  self,
617
619
  module: Module,
618
620
  *,
619
- priority: Priority | PriorityStr = Priority.get_default(),
621
+ priority: Priority | PriorityStr = Priority.get_default(), # noqa: B008
620
622
  unlock: bool = False,
621
623
  ) -> Iterator[Self]:
622
624
  self.use(module, priority=priority)
@@ -660,7 +662,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
660
662
  self.__channel.remove_listener(listener)
661
663
  return self
662
664
 
663
- def on_event(self, event: Event, /) -> ContextManager[None]:
665
+ def on_event(self, event: Event, /) -> AbstractContextManager[None]:
664
666
  self_event = ModuleEventProxy(self, event)
665
667
  return self.dispatch(self_event)
666
668
 
@@ -829,7 +831,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
829
831
  )
830
832
 
831
833
  __dependencies: Dependencies
832
- __lock: ContextManager[Any]
834
+ __lock: AbstractContextManager[Any]
833
835
  __owner: type | None
834
836
  __signature: Signature
835
837
  __tasks: deque[Callable[..., Any]]
@@ -838,11 +840,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
838
840
  def __init__(
839
841
  self,
840
842
  wrapped: Callable[P, T],
843
+ lock: RLock,
841
844
  /,
842
845
  threadsafe: bool | None = None,
843
846
  ) -> None:
844
847
  self.__dependencies = Dependencies.empty()
845
- self.__lock = get_lock(threadsafe)
848
+ self.__lock = lock_if_threadsafe(lock, threadsafe)
846
849
  self.__owner = None
847
850
  self.__tasks = deque()
848
851
  self.__wrapped = wrapped
@@ -904,8 +907,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
904
907
  self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
905
908
  return self
906
909
 
907
- def task[**_P, _T](self, wrapped: Callable[_P, _T] | None = None, /) -> Any:
908
- def decorator(wp: Callable[_P, _T]) -> Callable[_P, _T]:
910
+ def task[**TaskP, TaskR](
911
+ self,
912
+ wrapped: Callable[TaskP, TaskR] | None = None,
913
+ /,
914
+ ) -> Any:
915
+ def decorator(wp: Callable[TaskP, TaskR]) -> Callable[TaskP, TaskR]:
909
916
  self.__tasks.append(wp)
910
917
  return wp
911
918
 
@@ -920,7 +927,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
920
927
  return self
921
928
 
922
929
  @singledispatchmethod
923
- def on_event(self, event: Event, /) -> ContextManager[None] | None:
930
+ def on_event(self, event: Event, /) -> AbstractContextManager[None] | None:
924
931
  return None
925
932
 
926
933
  @on_event.register
@@ -941,7 +948,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
941
948
  arguments: dict[str, Any],
942
949
  additional_arguments: dict[str, Any],
943
950
  ) -> Arguments:
944
- bound = BoundArguments(self.signature, additional_arguments | arguments) # type: ignore[arg-type]
951
+ bound = BoundArguments(self.signature, additional_arguments | arguments)
945
952
  return Arguments(bound.args, bound.kwargs)
946
953
 
947
954
  def __run_tasks(self) -> None:
@@ -4,17 +4,23 @@ import itertools
4
4
  from abc import ABC, abstractmethod
5
5
  from collections import defaultdict
6
6
  from collections.abc import AsyncIterator, Collection, Iterator, Mapping, MutableMapping
7
- from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
7
+ from contextlib import (
8
+ AbstractAsyncContextManager,
9
+ AbstractContextManager,
10
+ AsyncExitStack,
11
+ ExitStack,
12
+ asynccontextmanager,
13
+ contextmanager,
14
+ )
8
15
  from contextvars import ContextVar
9
16
  from dataclasses import dataclass, field
10
17
  from enum import StrEnum
11
18
  from functools import partial
19
+ from threading import Lock
12
20
  from types import EllipsisType, TracebackType
13
21
  from typing import (
14
22
  TYPE_CHECKING,
15
23
  Any,
16
- AsyncContextManager,
17
- ContextManager,
18
24
  Final,
19
25
  Literal,
20
26
  NoReturn,
@@ -24,7 +30,7 @@ from typing import (
24
30
  runtime_checkable,
25
31
  )
26
32
 
27
- from injection._core.common.threading import get_lock
33
+ from injection._core.common.threading import lock_if_threadsafe
28
34
  from injection._core.slots import SlotKey
29
35
  from injection.exceptions import (
30
36
  InjectionError,
@@ -33,6 +39,8 @@ from injection.exceptions import (
33
39
  ScopeUndefinedError,
34
40
  )
35
41
 
42
+ __thread_lock: Final[Lock] = Lock()
43
+
36
44
 
37
45
  class ScopeKind(StrEnum):
38
46
  CONTEXTUAL = "contextual"
@@ -56,7 +64,7 @@ class ScopeResolver(Protocol):
56
64
  raise NotImplementedError
57
65
 
58
66
  @abstractmethod
59
- def bind(self, scope: Scope) -> ContextManager[None]:
67
+ def bind(self, scope: Scope) -> AbstractContextManager[None]:
60
68
  raise NotImplementedError
61
69
 
62
70
  @abstractmethod
@@ -128,7 +136,7 @@ __scope_resolvers: Final[Mapping[str, Mapping[str, ScopeResolver]]] = {
128
136
  async def adefine_scope(
129
137
  name: str,
130
138
  /,
131
- kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
139
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
132
140
  threadsafe: bool | None = None,
133
141
  ) -> AsyncIterator[ScopeFacade]:
134
142
  async with AsyncScope() as scope:
@@ -140,12 +148,11 @@ async def adefine_scope(
140
148
  def define_scope(
141
149
  name: str,
142
150
  /,
143
- kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
151
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
144
152
  threadsafe: bool | None = None,
145
153
  ) -> Iterator[ScopeFacade]:
146
- with SyncScope() as scope:
147
- with _bind_scope(name, scope, kind, threadsafe) as facade:
148
- yield facade
154
+ with SyncScope() as scope, _bind_scope(name, scope, kind, threadsafe) as facade:
155
+ yield facade
149
156
 
150
157
 
151
158
  if TYPE_CHECKING: # pragma: no cover
@@ -193,7 +200,7 @@ def _bind_scope(
193
200
  kind: ScopeKind | ScopeKindStr,
194
201
  threadsafe: bool | None,
195
202
  ) -> Iterator[ScopeFacade]:
196
- lock = get_lock(threadsafe)
203
+ lock = lock_if_threadsafe(__thread_lock, threadsafe)
197
204
 
198
205
  with lock:
199
206
  if get_first_scope(name, default=None):
@@ -205,7 +212,7 @@ def _bind_scope(
205
212
  stack.enter_context(__scope_resolvers[kind][name].bind(scope))
206
213
 
207
214
  try:
208
- yield _UserScope(scope, lock)
215
+ yield _UserScope(scope, lock_if_threadsafe(threadsafe=threadsafe))
209
216
 
210
217
  finally:
211
218
  with lock:
@@ -234,11 +241,11 @@ class Scope(Protocol):
234
241
  cache: MutableMapping[SlotKey[Any], Any]
235
242
 
236
243
  @abstractmethod
237
- async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
244
+ async def aenter[T](self, context_manager: AbstractAsyncContextManager[T]) -> T:
238
245
  raise NotImplementedError
239
246
 
240
247
  @abstractmethod
241
- def enter[T](self, context_manager: ContextManager[T]) -> T:
248
+ def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
242
249
  raise NotImplementedError
243
250
 
244
251
 
@@ -270,10 +277,10 @@ class AsyncScope(BaseScope[AsyncExitStack]):
270
277
  self.close()
271
278
  return await self.delegate.__aexit__(exc_type, exc_value, traceback)
272
279
 
273
- async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
280
+ async def aenter[T](self, context_manager: AbstractAsyncContextManager[T]) -> T:
274
281
  return await self.delegate.enter_async_context(context_manager)
275
282
 
276
- def enter[T](self, context_manager: ContextManager[T]) -> T:
283
+ def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
277
284
  return self.delegate.enter_context(context_manager)
278
285
 
279
286
 
@@ -296,10 +303,13 @@ class SyncScope(BaseScope[ExitStack]):
296
303
  self.close()
297
304
  return self.delegate.__exit__(exc_type, exc_value, traceback)
298
305
 
299
- async def aenter[T](self, context_manager: AsyncContextManager[T]) -> NoReturn:
306
+ async def aenter[T](
307
+ self,
308
+ context_manager: AbstractAsyncContextManager[T],
309
+ ) -> NoReturn:
300
310
  raise ScopeError("Synchronous scope doesn't support async context manager.")
301
311
 
302
- def enter[T](self, context_manager: ContextManager[T]) -> T:
312
+ def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
303
313
  return self.delegate.enter_context(context_manager)
304
314
 
305
315
 
@@ -319,7 +329,7 @@ class ScopeFacade(Protocol):
319
329
  @dataclass(repr=False, frozen=True, slots=True)
320
330
  class _UserScope(ScopeFacade):
321
331
  scope: Scope
322
- lock: ContextManager[Any]
332
+ lock: AbstractContextManager[Any]
323
333
 
324
334
  def set_slot[T](self, key: SlotKey[T], value: T) -> Self:
325
335
  return self.slot_map({key: value})
@@ -194,18 +194,18 @@ class EntrypointBuilder[**P, T1, T2](_EntrypointDecorator[P, T1, T2]):
194
194
 
195
195
  return decorator(wrapped) if wrapped else decorator
196
196
 
197
- def async_to_sync[_T](
198
- self: EntrypointBuilder[P, T1, Awaitable[_T]],
199
- run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
197
+ def async_to_sync[T](
198
+ self: EntrypointBuilder[P, T1, Awaitable[T]],
199
+ run: Callable[[Coroutine[Any, Any, T]], T] = asyncio.run,
200
200
  /,
201
- ) -> EntrypointBuilder[P, T1, _T]:
201
+ ) -> EntrypointBuilder[P, T1, T]:
202
202
  return self._add_rule(_AsyncToSyncRule(run)) # type: ignore[arg-type]
203
203
 
204
- def decorate[_T](
204
+ def decorate[T](
205
205
  self,
206
- decorator: Callable[[Callable[P, T2]], Callable[P, _T]],
206
+ decorator: Callable[[Callable[P, T2]], Callable[P, T]],
207
207
  /,
208
- ) -> EntrypointBuilder[P, T1, _T]:
208
+ ) -> EntrypointBuilder[P, T1, T]:
209
209
  return self._add_rule(_DecorateRule(decorator))
210
210
 
211
211
  def inject(self) -> Self:
@@ -224,10 +224,10 @@ class EntrypointBuilder[**P, T1, T2](_EntrypointDecorator[P, T1, T2]):
224
224
  self._add_rule(_LoadProfileRule(self.profile_loader, name))
225
225
  return self
226
226
 
227
- def _add_rule[_T](
227
+ def _add_rule[T](
228
228
  self,
229
- rule: Rule[P, T2, _T],
230
- ) -> EntrypointBuilder[P, T1, _T]:
229
+ rule: Rule[P, T2, T],
230
+ ) -> EntrypointBuilder[P, T1, T]:
231
231
  self.__rules.append(rule)
232
232
  return self # type: ignore[return-value]
233
233
 
@@ -10,12 +10,12 @@ dev = [
10
10
  "ruff",
11
11
  ]
12
12
  docs = [
13
- "mkdocs",
14
- "mkdocs-material",
13
+ "ghp-import",
14
+ "zensical",
15
15
  ]
16
16
  test = [
17
17
  "fastapi",
18
- "httpx",
18
+ "httpx2",
19
19
  "pydantic",
20
20
  "pytest",
21
21
  "pytest-asyncio",
@@ -24,7 +24,7 @@ test = [
24
24
 
25
25
  [project]
26
26
  name = "python-injection"
27
- version = "0.26.2"
27
+ version = "0.26.4"
28
28
  description = "Dead-simple dependency injection framework for Python."
29
29
  license = "MIT"
30
30
  license-files = ["LICENSE"]
@@ -44,6 +44,7 @@ classifiers = [
44
44
  "Programming Language :: Python :: 3.12",
45
45
  "Programming Language :: Python :: 3.13",
46
46
  "Programming Language :: Python :: 3.14",
47
+ "Programming Language :: Python :: 3.15",
47
48
  "Operating System :: OS Independent",
48
49
  "Intended Audience :: Developers",
49
50
  "Natural Language :: English",
@@ -1,13 +0,0 @@
1
- from contextlib import nullcontext
2
- from os import getenv
3
- from threading import RLock
4
- from typing import Any, ContextManager, Final
5
-
6
- _PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
7
- int(getenv("PYTHON_INJECTION_THREADSAFE", 0))
8
- )
9
-
10
-
11
- def get_lock(threadsafe: bool | None = None) -> ContextManager[Any]:
12
- threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
13
- return RLock() if threadsafe else nullcontext()