python-injection 0.26.3__tar.gz → 0.27.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.
Files changed (31) hide show
  1. {python_injection-0.26.3 → python_injection-0.27.0}/PKG-INFO +8 -8
  2. {python_injection-0.26.3 → python_injection-0.27.0}/docs/index.md +6 -6
  3. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/asynchronous.py +3 -3
  4. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/threading.py +12 -3
  5. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/injectables.py +5 -5
  6. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/module.py +17 -9
  7. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/scope.py +6 -3
  8. {python_injection-0.26.3 → python_injection-0.27.0}/injection/loaders.py +8 -0
  9. {python_injection-0.26.3 → python_injection-0.27.0}/pyproject.toml +4 -4
  10. {python_injection-0.26.3 → python_injection-0.27.0}/.gitignore +0 -0
  11. {python_injection-0.26.3 → python_injection-0.27.0}/LICENSE +0 -0
  12. {python_injection-0.26.3 → python_injection-0.27.0}/injection/__init__.py +0 -0
  13. {python_injection-0.26.3 → python_injection-0.27.0}/injection/__init__.pyi +0 -0
  14. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/__init__.py +0 -0
  15. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/asfunction.py +0 -0
  16. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/__init__.py +0 -0
  17. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/event.py +0 -0
  18. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/invertible.py +0 -0
  19. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/lazy.py +0 -0
  20. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/common/type.py +0 -0
  21. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/descriptors.py +0 -0
  22. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/locator.py +0 -0
  23. {python_injection-0.26.3 → python_injection-0.27.0}/injection/_core/slots.py +0 -0
  24. {python_injection-0.26.3 → python_injection-0.27.0}/injection/entrypoint.py +0 -0
  25. {python_injection-0.26.3 → python_injection-0.27.0}/injection/exceptions.py +0 -0
  26. {python_injection-0.26.3 → python_injection-0.27.0}/injection/ext/__init__.py +0 -0
  27. {python_injection-0.26.3 → python_injection-0.27.0}/injection/ext/fastapi.py +0 -0
  28. {python_injection-0.26.3 → python_injection-0.27.0}/injection/ext/fastapi.pyi +0 -0
  29. {python_injection-0.26.3 → python_injection-0.27.0}/injection/py.typed +0 -0
  30. {python_injection-0.26.3 → python_injection-0.27.0}/injection/testing/__init__.py +0 -0
  31. {python_injection-0.26.3 → python_injection-0.27.0}/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.3
3
+ Version: 0.27.0
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
@@ -32,12 +32,12 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  # python-injection
34
34
 
35
- [![PyPI - Version](https://img.shields.io/pypi/v/python-injection.svg?color=546d78&style=for-the-badge)](https://pypi.org/project/python-injection)
36
- [![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)
37
37
 
38
38
  ## Project motivations
39
39
 
40
- Dependency injection in Python has long been a source of frustration.
40
+ Dependency injection in Python has long been a source of frustration.
41
41
 
42
42
  Existing solutions are often verbose, require extensive boilerplate, or fail to leverage Python's type hints effectively. `python-injection` was created to solve these problems once and for all by providing a simple, elegant, and powerful dependency injection framework that feels natural to Python developers.
43
43
 
@@ -47,13 +47,13 @@ The goal is straightforward: make dependency injection so easy that you'll wonde
47
47
 
48
48
  - **Type-driven resolution**: Dependencies are automatically resolved using Python's type annotations.
49
49
  - **Decorator-based registration**: Register your dependencies with simple, readable decorators.
50
- - **Flexible lifetimes**: Choose from 4 type of lifetimes to match your needs:
50
+ - **Flexible lifetimes**: Choose from four types of lifetimes to match your needs:
51
51
  - **Transient**: A new instance every time
52
52
  - **Singleton**: One instance for the entire application
53
53
  - **Scoped**: One instance per scope, with context manager support
54
- - **Constant**: Register pre-existing values
54
+ - **Constant**: A value created once and never re-resolved, either pre-existing or computed lazily
55
55
  - **Profile support**: Ability to swap certain dependencies based on a profile.
56
- - **Pull-based instantiation**: Dependencies are only created when needed, improving startup time and resource usage.
56
+ - **Pull-based instantiation**: Dependencies registered with decorators are only created when needed, improving startup time and resource usage.
57
57
  - **Full sync/async support**: Works seamlessly with both synchronous and asynchronous code.
58
58
 
59
59
  ## Installation
@@ -1,11 +1,11 @@
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
 
8
- Dependency injection in Python has long been a source of frustration.
8
+ Dependency injection in Python has long been a source of frustration.
9
9
 
10
10
  Existing solutions are often verbose, require extensive boilerplate, or fail to leverage Python's type hints effectively. `python-injection` was created to solve these problems once and for all by providing a simple, elegant, and powerful dependency injection framework that feels natural to Python developers.
11
11
 
@@ -15,13 +15,13 @@ The goal is straightforward: make dependency injection so easy that you'll wonde
15
15
 
16
16
  - **Type-driven resolution**: Dependencies are automatically resolved using Python's type annotations.
17
17
  - **Decorator-based registration**: Register your dependencies with simple, readable decorators.
18
- - **Flexible lifetimes**: Choose from 4 type of lifetimes to match your needs:
18
+ - **Flexible lifetimes**: Choose from four types of lifetimes to match your needs:
19
19
  - **Transient**: A new instance every time
20
20
  - **Singleton**: One instance for the entire application
21
21
  - **Scoped**: One instance per scope, with context manager support
22
- - **Constant**: Register pre-existing values
22
+ - **Constant**: A value created once and never re-resolved, either pre-existing or computed lazily
23
23
  - **Profile support**: Ability to swap certain dependencies based on a profile.
24
- - **Pull-based instantiation**: Dependencies are only created when needed, improving startup time and resource usage.
24
+ - **Pull-based instantiation**: Dependencies registered with decorators are only created when needed, improving startup time and resource usage.
25
25
  - **Full sync/async support**: Works seamlessly with both synchronous and asynchronous code.
26
26
 
27
27
  ## Installation
@@ -4,7 +4,7 @@ from contextlib import AbstractAsyncContextManager
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, NoReturn, Protocol, runtime_checkable
6
6
 
7
- AsyncSemaphore: Callable[[int], AbstractAsyncContextManager[Any]]
7
+ AsyncLock: Callable[[], AbstractAsyncContextManager[Any]]
8
8
 
9
9
  try:
10
10
  import anyio
@@ -12,10 +12,10 @@ try:
12
12
  except ImportError: # pragma: no cover
13
13
  import asyncio
14
14
 
15
- AsyncSemaphore = asyncio.Semaphore
15
+ AsyncLock = asyncio.Lock
16
16
 
17
17
  else:
18
- AsyncSemaphore = anyio.Semaphore
18
+ AsyncLock = anyio.Lock
19
19
 
20
20
 
21
21
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from contextlib import AbstractContextManager, nullcontext
2
4
  from os import getenv
3
- from threading import RLock
5
+ from threading import Lock
4
6
  from typing import Any, Final
5
7
 
6
8
  _PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
@@ -8,6 +10,13 @@ _PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
8
10
  )
9
11
 
10
12
 
11
- def get_lock(threadsafe: bool | None = None) -> AbstractContextManager[Any]:
13
+ def lock_if_threadsafe(
14
+ lock: AbstractContextManager[Any] | None = None,
15
+ threadsafe: bool | None = None,
16
+ ) -> AbstractContextManager[Any]:
12
17
  threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
13
- return RLock() if threadsafe else nullcontext()
18
+
19
+ if not threadsafe:
20
+ return nullcontext()
21
+
22
+ return lock or Lock()
@@ -12,7 +12,7 @@ from typing import (
12
12
  runtime_checkable,
13
13
  )
14
14
 
15
- from injection._core.common.asynchronous import AsyncSemaphore, Caller
15
+ from injection._core.common.asynchronous import AsyncLock, Caller
16
16
  from injection._core.common.type import InputType
17
17
  from injection._core.scope import Scope, get_first_scope, in_scope_cache
18
18
  from injection._core.slots import SlotKey
@@ -51,12 +51,12 @@ class TransientInjectable[T](Injectable[T]):
51
51
 
52
52
 
53
53
  class CacheLogic[T]:
54
- __slots__ = ("__semaphore",)
54
+ __slots__ = ("__lock",)
55
55
 
56
- __semaphore: AbstractAsyncContextManager[Any]
56
+ __lock: AbstractAsyncContextManager[Any]
57
57
 
58
58
  def __init__(self) -> None:
59
- self.__semaphore = AsyncSemaphore(1)
59
+ self.__lock = AsyncLock()
60
60
 
61
61
  async def aget_or_create[K](
62
62
  self,
@@ -64,7 +64,7 @@ class CacheLogic[T]:
64
64
  key: K,
65
65
  factory: Callable[..., Awaitable[T]],
66
66
  ) -> T:
67
- async with self.__semaphore:
67
+ async with self.__lock:
68
68
  with suppress(KeyError):
69
69
  return cache[key]
70
70
 
@@ -35,6 +35,7 @@ from inspect import (
35
35
  )
36
36
  from inspect import signature as inspect_signature
37
37
  from logging import Logger, getLogger
38
+ from threading import RLock
38
39
  from types import MethodType
39
40
  from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, overload
40
41
 
@@ -44,7 +45,7 @@ from injection._core.common.asynchronous import Caller, SimpleAwaitable
44
45
  from injection._core.common.event import Event, EventChannel, EventListener
45
46
  from injection._core.common.invertible import Invertible, SimpleInvertible
46
47
  from injection._core.common.lazy import Lazy
47
- from injection._core.common.threading import get_lock
48
+ from injection._core.common.threading import lock_if_threadsafe
48
49
  from injection._core.common.type import (
49
50
  InputType,
50
51
  TypeInfo,
@@ -180,6 +181,11 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
180
181
  init=False,
181
182
  repr=False,
182
183
  )
184
+ __lock: RLock = field(
185
+ default_factory=RLock,
186
+ init=False,
187
+ repr=False,
188
+ )
183
189
  __loggers: list[Logger] = field(
184
190
  default_factory=lambda: [getLogger("python-injection")],
185
191
  init=False,
@@ -350,7 +356,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
350
356
  /,
351
357
  threadsafe: bool | None = None,
352
358
  ) -> InjectMetadata[P, T]:
353
- return InjectMetadata(wrapped, threadsafe).listen(self)
359
+ return InjectMetadata(wrapped, self.__lock, threadsafe).listen(self)
354
360
 
355
361
  if TYPE_CHECKING: # pragma: no cover
356
362
 
@@ -397,7 +403,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
397
403
  *,
398
404
  threadsafe: bool | None = None,
399
405
  ) -> T:
400
- with get_lock(threadsafe):
406
+ with lock_if_threadsafe(self.__lock, threadsafe):
401
407
  injectable = self[cls]
402
408
  return await injectable.aget_instance()
403
409
 
@@ -407,7 +413,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
407
413
  *,
408
414
  threadsafe: bool | None = None,
409
415
  ) -> T:
410
- with get_lock(threadsafe):
416
+ with lock_if_threadsafe(self.__lock, threadsafe):
411
417
  injectable = self[cls]
412
418
  return injectable.get_instance()
413
419
 
@@ -598,11 +604,12 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
598
604
  return self
599
605
 
600
606
  def stop_using(self, module: Module) -> Self:
601
- event = ModuleRemoved(self, module)
607
+ if module in self.__modules:
608
+ event = ModuleRemoved(self, module)
602
609
 
603
- with suppress(KeyError), self.dispatch(event):
604
- self.__modules.pop(module)
605
- module.remove_listener(self)
610
+ with self.dispatch(event):
611
+ self.__modules.pop(module)
612
+ module.remove_listener(self)
606
613
 
607
614
  return self
608
615
 
@@ -833,11 +840,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
833
840
  def __init__(
834
841
  self,
835
842
  wrapped: Callable[P, T],
843
+ lock: RLock,
836
844
  /,
837
845
  threadsafe: bool | None = None,
838
846
  ) -> None:
839
847
  self.__dependencies = Dependencies.empty()
840
- self.__lock = get_lock(threadsafe)
848
+ self.__lock = lock_if_threadsafe(lock, threadsafe)
841
849
  self.__owner = None
842
850
  self.__tasks = deque()
843
851
  self.__wrapped = wrapped
@@ -16,6 +16,7 @@ from contextvars import ContextVar
16
16
  from dataclasses import dataclass, field
17
17
  from enum import StrEnum
18
18
  from functools import partial
19
+ from threading import Lock
19
20
  from types import EllipsisType, TracebackType
20
21
  from typing import (
21
22
  TYPE_CHECKING,
@@ -29,7 +30,7 @@ from typing import (
29
30
  runtime_checkable,
30
31
  )
31
32
 
32
- from injection._core.common.threading import get_lock
33
+ from injection._core.common.threading import lock_if_threadsafe
33
34
  from injection._core.slots import SlotKey
34
35
  from injection.exceptions import (
35
36
  InjectionError,
@@ -38,6 +39,8 @@ from injection.exceptions import (
38
39
  ScopeUndefinedError,
39
40
  )
40
41
 
42
+ __thread_lock: Final[Lock] = Lock()
43
+
41
44
 
42
45
  class ScopeKind(StrEnum):
43
46
  CONTEXTUAL = "contextual"
@@ -197,7 +200,7 @@ def _bind_scope(
197
200
  kind: ScopeKind | ScopeKindStr,
198
201
  threadsafe: bool | None,
199
202
  ) -> Iterator[ScopeFacade]:
200
- lock = get_lock(threadsafe)
203
+ lock = lock_if_threadsafe(__thread_lock, threadsafe)
201
204
 
202
205
  with lock:
203
206
  if get_first_scope(name, default=None):
@@ -209,7 +212,7 @@ def _bind_scope(
209
212
  stack.enter_context(__scope_resolvers[kind][name].bind(scope))
210
213
 
211
214
  try:
212
- yield _UserScope(scope, lock)
215
+ yield _UserScope(scope, lock_if_threadsafe(threadsafe=threadsafe))
213
216
 
214
217
  finally:
215
218
  with lock:
@@ -5,6 +5,7 @@ import sys
5
5
  from abc import abstractmethod
6
6
  from collections.abc import Callable, Iterator, Mapping, Sequence
7
7
  from dataclasses import dataclass, field
8
+ from fnmatch import fnmatchcase
8
9
  from importlib import import_module
9
10
  from importlib.util import find_spec
10
11
  from os.path import isfile
@@ -99,6 +100,13 @@ class PythonModuleLoader:
99
100
  module = import_module(name) if self.predicate(name) else None
100
101
  yield name, module
101
102
 
103
+ @classmethod
104
+ def fnmatch(cls, *patterns: str) -> Self:
105
+ def predicate(module_name: str) -> bool:
106
+ return any(fnmatchcase(module_name, pattern) for pattern in patterns)
107
+
108
+ return cls(predicate)
109
+
102
110
  @classmethod
103
111
  def from_keywords(cls, *keywords: str) -> Self:
104
112
  """
@@ -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.3"
27
+ version = "0.27.0"
28
28
  description = "Dead-simple dependency injection framework for Python."
29
29
  license = "MIT"
30
30
  license-files = ["LICENSE"]