python-injection 0.14.2__tar.gz → 0.14.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 (28) hide show
  1. {python_injection-0.14.2 → python_injection-0.14.4}/PKG-INFO +1 -1
  2. {python_injection-0.14.2 → python_injection-0.14.4}/injection/__init__.py +4 -0
  3. {python_injection-0.14.2 → python_injection-0.14.4}/injection/__init__.pyi +14 -0
  4. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/type.py +7 -1
  5. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/injectables.py +10 -4
  6. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/module.py +54 -35
  7. python_injection-0.14.4/injection/_core/slots.py +24 -0
  8. {python_injection-0.14.2 → python_injection-0.14.4}/injection/exceptions.py +4 -4
  9. python_injection-0.14.4/injection/py.typed +0 -0
  10. {python_injection-0.14.2 → python_injection-0.14.4}/injection/testing/__init__.py +2 -0
  11. {python_injection-0.14.2 → python_injection-0.14.4}/injection/testing/__init__.pyi +1 -0
  12. {python_injection-0.14.2 → python_injection-0.14.4}/pyproject.toml +1 -1
  13. python_injection-0.14.2/injection/_core/__init__.py +0 -53
  14. python_injection-0.14.2/injection/_core/hook.py +0 -106
  15. {python_injection-0.14.2 → python_injection-0.14.4}/.gitignore +0 -0
  16. {python_injection-0.14.2 → python_injection-0.14.4}/README.md +0 -0
  17. {python_injection-0.14.2/injection/_core/common → python_injection-0.14.4/injection/_core}/__init__.py +0 -0
  18. {python_injection-0.14.2/injection/integrations → python_injection-0.14.4/injection/_core/common}/__init__.py +0 -0
  19. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/asynchronous.py +0 -0
  20. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/event.py +0 -0
  21. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/invertible.py +0 -0
  22. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/key.py +0 -0
  23. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/common/lazy.py +0 -0
  24. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/descriptors.py +0 -0
  25. {python_injection-0.14.2 → python_injection-0.14.4}/injection/_core/scope.py +0 -0
  26. /python_injection-0.14.2/injection/py.typed → /python_injection-0.14.4/injection/integrations/__init__.py +0 -0
  27. {python_injection-0.14.2 → python_injection-0.14.4}/injection/integrations/fastapi.py +0 -0
  28. {python_injection-0.14.2 → python_injection-0.14.4}/injection/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.14.2
3
+ Version: 0.14.4
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -2,6 +2,7 @@ from ._core.descriptors import LazyInstance
2
2
  from ._core.injectables import Injectable
3
3
  from ._core.module import Mode, Module, Priority, mod
4
4
  from ._core.scope import adefine_scope, define_scope
5
+ from ._core.slots import Slot
5
6
 
6
7
  __all__ = (
7
8
  "Injectable",
@@ -9,6 +10,7 @@ __all__ = (
9
10
  "Mode",
10
11
  "Module",
11
12
  "Priority",
13
+ "Slot",
12
14
  "adefine_scope",
13
15
  "afind_instance",
14
16
  "aget_instance",
@@ -21,6 +23,7 @@ __all__ = (
21
23
  "inject",
22
24
  "injectable",
23
25
  "mod",
26
+ "reserve_scoped_slot",
24
27
  "scoped",
25
28
  "set_constant",
26
29
  "should_be_injectable",
@@ -36,6 +39,7 @@ get_instance = mod().get_instance
36
39
  get_lazy_instance = mod().get_lazy_instance
37
40
  inject = mod().inject
38
41
  injectable = mod().injectable
42
+ reserve_scoped_slot = mod().reserve_scoped_slot
39
43
  scoped = mod().scoped
40
44
  set_constant = mod().set_constant
41
45
  should_be_injectable = mod().should_be_injectable
@@ -23,6 +23,7 @@ get_instance = __MODULE.get_instance
23
23
  get_lazy_instance = __MODULE.get_lazy_instance
24
24
  inject = __MODULE.inject
25
25
  injectable = __MODULE.injectable
26
+ reserve_scoped_slot = __MODULE.reserve_scoped_slot
26
27
  scoped = __MODULE.scoped
27
28
  set_constant = __MODULE.set_constant
28
29
  should_be_injectable = __MODULE.should_be_injectable
@@ -46,6 +47,11 @@ class Injectable[T](Protocol):
46
47
  @abstractmethod
47
48
  def get_instance(self) -> T: ...
48
49
 
50
+ @runtime_checkable
51
+ class Slot[T](Protocol):
52
+ @abstractmethod
53
+ def set(self, instance: T, /) -> Self: ...
54
+
49
55
  class LazyInstance[T]:
50
56
  def __init__(
51
57
  self,
@@ -171,6 +177,14 @@ class Module:
171
177
  that no dependencies are resolved, so the module doesn't need to be locked.
172
178
  """
173
179
 
180
+ def reserve_scoped_slot[T](
181
+ self,
182
+ on: _TypeInfo[T],
183
+ /,
184
+ scope_name: str,
185
+ *,
186
+ mode: Mode | ModeStr = ...,
187
+ ) -> Slot[T]: ...
174
188
  def make_injected_function[**P, T](
175
189
  self,
176
190
  wrapped: Callable[P, T],
@@ -2,6 +2,7 @@ from collections.abc import (
2
2
  AsyncGenerator,
3
3
  AsyncIterable,
4
4
  AsyncIterator,
5
+ Awaitable,
5
6
  Callable,
6
7
  Generator,
7
8
  Iterable,
@@ -21,7 +22,12 @@ from typing import (
21
22
 
22
23
  type TypeDef[T] = type[T] | TypeAliasType | GenericAlias
23
24
  type InputType[T] = TypeDef[T] | UnionType
24
- type TypeInfo[T] = InputType[T] | Callable[..., T] | Iterable[TypeInfo[T]]
25
+ type TypeInfo[T] = (
26
+ InputType[T]
27
+ | Callable[..., T]
28
+ | Callable[..., Awaitable[T]]
29
+ | Iterable[TypeInfo[T]]
30
+ )
25
31
 
26
32
 
27
33
  def get_return_types(*args: TypeInfo[Any]) -> Iterator[InputType[Any]]:
@@ -107,25 +107,31 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
107
107
  raise NotImplementedError
108
108
 
109
109
  async def aget_instance(self) -> T:
110
- scope = get_scope(self.scope_name)
110
+ scope = self.get_scope()
111
111
 
112
112
  with suppress(KeyError):
113
113
  return scope.cache[self]
114
114
 
115
115
  instance = await self.abuild(scope)
116
- scope.cache[self] = instance
116
+ self.set_instance(instance, scope)
117
117
  return instance
118
118
 
119
119
  def get_instance(self) -> T:
120
- scope = get_scope(self.scope_name)
120
+ scope = self.get_scope()
121
121
 
122
122
  with suppress(KeyError):
123
123
  return scope.cache[self]
124
124
 
125
125
  instance = self.build(scope)
126
- scope.cache[self] = instance
126
+ self.set_instance(instance, scope)
127
127
  return instance
128
128
 
129
+ def get_scope(self) -> Scope:
130
+ return get_scope(self.scope_name)
131
+
132
+ def set_instance(self, instance: T, scope: Scope) -> None:
133
+ scope.cache[self] = instance
134
+
129
135
  def unlock(self) -> None:
130
136
  if self.is_locked:
131
137
  raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
@@ -16,7 +16,7 @@ from collections.abc import (
16
16
  from contextlib import asynccontextmanager, contextmanager, nullcontext, suppress
17
17
  from dataclasses import dataclass, field
18
18
  from enum import StrEnum
19
- from functools import partialmethod, singledispatchmethod, update_wrapper
19
+ from functools import partial, partialmethod, singledispatchmethod, update_wrapper
20
20
  from inspect import (
21
21
  Signature,
22
22
  isasyncgenfunction,
@@ -57,18 +57,21 @@ from injection._core.common.type import (
57
57
  TypeInfo,
58
58
  get_return_types,
59
59
  get_yield_hint,
60
+ standardize_types,
60
61
  )
61
- from injection._core.hook import Hook, apply_hooks
62
62
  from injection._core.injectables import (
63
63
  AsyncCMScopedInjectable,
64
64
  CMScopedInjectable,
65
65
  Injectable,
66
+ ScopedInjectable,
66
67
  ShouldBeInjectable,
67
68
  SimpleInjectable,
68
69
  SimpleScopedInjectable,
69
70
  SingletonInjectable,
70
71
  )
72
+ from injection._core.slots import ScopedSlot, Slot
71
73
  from injection.exceptions import (
74
+ EmptySlotError,
72
75
  ModuleError,
73
76
  ModuleLockError,
74
77
  ModuleNotUsedError,
@@ -222,21 +225,19 @@ class Updater[T]:
222
225
  def make_record(self) -> Record[T]:
223
226
  return Record(self.injectable, self.mode)
224
227
 
225
-
226
- @dataclass(repr=False, eq=False, frozen=True, slots=True)
227
- class LocatorHooks[T]:
228
- on_conflict: Hook[[Record[T], Record[T], InputType[T]], bool] = field(
229
- default_factory=Hook,
230
- init=False,
231
- )
232
- on_input: Hook[[Iterable[InputType[T]]], Iterable[InputType[T]]] = field(
233
- default_factory=Hook,
234
- init=False,
235
- )
236
- on_update: Hook[[Updater[T]], Updater[T]] = field(
237
- default_factory=Hook,
238
- init=False,
239
- )
228
+ @classmethod
229
+ def with_basics(
230
+ cls,
231
+ on: TypeInfo[T],
232
+ /,
233
+ injectable: Injectable[T],
234
+ mode: Mode | ModeStr,
235
+ ) -> Self:
236
+ return cls(
237
+ classes=get_return_types(on),
238
+ injectable=injectable,
239
+ mode=Mode(mode),
240
+ )
240
241
 
241
242
 
242
243
  @dataclass(repr=False, frozen=True, slots=True)
@@ -250,8 +251,6 @@ class Locator(Broker):
250
251
  init=False,
251
252
  )
252
253
 
253
- static_hooks: ClassVar[LocatorHooks[Any]] = LocatorHooks()
254
-
255
254
  def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
256
255
  for input_class in self.__standardize_inputs((cls,)):
257
256
  try:
@@ -326,25 +325,30 @@ class Locator(Broker):
326
325
 
327
326
  yield cls, record
328
327
 
328
+ @staticmethod
329
329
  def __keep_new_record[T](
330
- self,
331
330
  new: Record[T],
332
331
  existing: Record[T],
333
332
  cls: InputType[T],
334
333
  ) -> bool:
335
- return apply_hooks(
336
- lambda *args, **kwargs: False,
337
- self.static_hooks.on_conflict,
338
- )(new, existing, cls)
334
+ new_mode, existing_mode = new.mode, existing.mode
335
+ is_override = new_mode == Mode.OVERRIDE
339
336
 
337
+ if new_mode == existing_mode and not is_override:
338
+ raise RuntimeError(f"An injectable already exists for the class `{cls}`.")
339
+
340
+ return is_override or new_mode.rank > existing_mode.rank
341
+
342
+ @staticmethod
340
343
  def __standardize_inputs[T](
341
- self,
342
344
  classes: Iterable[InputType[T]],
343
345
  ) -> Iterable[InputType[T]]:
344
- return apply_hooks(lambda c: c, self.static_hooks.on_input)(classes)
346
+ return tuple(standardize_types(*classes, with_origin=True))
345
347
 
346
- def __update_preprocessing[T](self, updater: Updater[T]) -> Updater[T]:
347
- return apply_hooks(lambda u: u, self.static_hooks.on_update)(updater)
348
+ @staticmethod
349
+ def __update_preprocessing[T](updater: Updater[T]) -> Updater[T]:
350
+ updater.classes = frozenset(standardize_types(*updater.classes))
351
+ return updater
348
352
 
349
353
 
350
354
  """
@@ -433,12 +437,9 @@ class Module(Broker, EventListener):
433
437
  ) -> Any:
434
438
  def decorator(wp: Recipe[P, T]) -> Recipe[P, T]:
435
439
  factory = extract_caller(self.make_injected_function(wp) if inject else wp)
440
+ injectable = cls(factory) # type: ignore[arg-type]
436
441
  hints = on if ignore_type_hint else (wp, on)
437
- updater = Updater(
438
- classes=get_return_types(hints),
439
- injectable=cls(factory), # type: ignore[arg-type]
440
- mode=Mode(mode),
441
- )
442
+ updater = Updater.with_basics(hints, injectable, mode)
442
443
  self.update(updater)
443
444
  return wp
444
445
 
@@ -458,7 +459,7 @@ class Module(Broker, EventListener):
458
459
  def decorator(
459
460
  wrapped: Recipe[P, T] | GeneratorRecipe[P, T],
460
461
  ) -> Recipe[P, T] | GeneratorRecipe[P, T]:
461
- injectable_class: Callable[[Caller[P, Any], str], Injectable[T]]
462
+ injectable_class: type[ScopedInjectable[Any, T]]
462
463
  wrapper: Recipe[P, T] | ContextManagerLikeRecipe[P, T]
463
464
 
464
465
  if isasyncgenfunction(wrapped):
@@ -478,7 +479,7 @@ class Module(Broker, EventListener):
478
479
  hints = on if hint is None else (hint, on)
479
480
  self.injectable(
480
481
  wrapper,
481
- cls=lambda factory: injectable_class(factory, scope_name),
482
+ cls=partial(injectable_class, scope_name=scope_name),
482
483
  ignore_type_hint=True,
483
484
  inject=inject,
484
485
  on=hints,
@@ -539,6 +540,24 @@ class Module(Broker, EventListener):
539
540
  )
540
541
  return self
541
542
 
543
+ def reserve_scoped_slot[T](
544
+ self,
545
+ on: TypeInfo[T],
546
+ /,
547
+ scope_name: str,
548
+ *,
549
+ mode: Mode | ModeStr = Mode.get_default(),
550
+ ) -> Slot[T]:
551
+ def when_empty() -> T:
552
+ raise EmptySlotError(
553
+ f"The slot for `{on}` is unset in the current `{scope_name}` scope."
554
+ )
555
+
556
+ injectable = SimpleScopedInjectable(SyncCaller(when_empty), scope_name)
557
+ updater = Updater.with_basics(on, injectable, mode)
558
+ self.update(updater)
559
+ return ScopedSlot(injectable)
560
+
542
561
  def inject[**P, T](
543
562
  self,
544
563
  wrapped: Callable[P, T] | None = None,
@@ -0,0 +1,24 @@
1
+ from abc import abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Any, Protocol, runtime_checkable
4
+
5
+ from injection._core.injectables import ScopedInjectable
6
+
7
+
8
+ @runtime_checkable
9
+ class Slot[T](Protocol):
10
+ __slots__ = ()
11
+
12
+ @abstractmethod
13
+ def set(self, instance: T, /) -> None:
14
+ raise NotImplementedError
15
+
16
+
17
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
18
+ class ScopedSlot[T](Slot[T]):
19
+ injectable: ScopedInjectable[Any, T]
20
+
21
+ def set(self, instance: T, /) -> None:
22
+ injectable = self.injectable
23
+ scope = injectable.get_scope()
24
+ injectable.set_instance(instance, scope)
@@ -1,7 +1,7 @@
1
1
  from typing import Any
2
2
 
3
3
  __all__ = (
4
- "HookError",
4
+ "EmptySlotError",
5
5
  "InjectionError",
6
6
  "ModuleError",
7
7
  "ModuleLockError",
@@ -34,6 +34,9 @@ class NoInjectable[T](KeyError, InjectionError):
34
34
  class SkipInjectable(InjectionError): ...
35
35
 
36
36
 
37
+ class EmptySlotError(SkipInjectable, InjectionError): ...
38
+
39
+
37
40
  class ModuleError(InjectionError): ...
38
41
 
39
42
 
@@ -50,6 +53,3 @@ class ScopeUndefinedError(LookupError, SkipInjectable, ScopeError): ...
50
53
 
51
54
 
52
55
  class ScopeAlreadyDefinedError(ScopeError): ...
53
-
54
-
55
- class HookError(InjectionError): ...
File without changes
@@ -5,6 +5,7 @@ from injection.utils import load_profile
5
5
 
6
6
  __all__ = (
7
7
  "load_test_profile",
8
+ "reserve_scoped_test_slot",
8
9
  "set_test_constant",
9
10
  "should_be_test_injectable",
10
11
  "test_constant",
@@ -15,6 +16,7 @@ __all__ = (
15
16
 
16
17
  _TEST_PROFILE_NAME: Final[str] = "__testing__"
17
18
 
19
+ reserve_scoped_test_slot = mod(_TEST_PROFILE_NAME).reserve_scoped_slot
18
20
  set_test_constant = mod(_TEST_PROFILE_NAME).set_constant
19
21
  should_be_test_injectable = mod(_TEST_PROFILE_NAME).should_be_injectable
20
22
  test_constant = mod(_TEST_PROFILE_NAME).constant
@@ -4,6 +4,7 @@ from injection import Module
4
4
 
5
5
  __MODULE: Final[Module] = ...
6
6
 
7
+ reserve_scoped_test_slot = __MODULE.reserve_scoped_slot
7
8
  set_test_constant = __MODULE.set_constant
8
9
  should_be_test_injectable = __MODULE.should_be_injectable
9
10
  test_constant = __MODULE.constant
@@ -24,7 +24,7 @@ test = [
24
24
 
25
25
  [project]
26
26
  name = "python-injection"
27
- version = "0.14.2"
27
+ version = "0.14.4"
28
28
  description = "Fast and easy dependency injection framework."
29
29
  license = { text = "MIT" }
30
30
  readme = "README.md"
@@ -1,53 +0,0 @@
1
- from collections.abc import Iterable
2
- from typing import Any
3
-
4
- from injection._core.common.type import InputType, standardize_types
5
- from injection._core.hook import HookGenerator
6
- from injection._core.module import Locator, Mode, Record, Updater
7
-
8
- __all__ = ()
9
-
10
-
11
- @Locator.static_hooks.on_conflict
12
- def check_mode[T](
13
- new: Record[T],
14
- existing: Record[T],
15
- cls: InputType[T],
16
- *_: Any,
17
- **__: Any,
18
- ) -> HookGenerator[bool]:
19
- new_mode = new.mode
20
- is_override = new_mode == Mode.OVERRIDE
21
-
22
- if new_mode == existing.mode and not is_override:
23
- raise RuntimeError(f"An injectable already exists for the class `{cls}`.")
24
-
25
- value = yield
26
- return value or is_override
27
-
28
-
29
- @Locator.static_hooks.on_conflict
30
- def compare_mode_rank[T](
31
- new: Record[T],
32
- existing: Record[T],
33
- *_: Any,
34
- **__: Any,
35
- ) -> HookGenerator[bool]:
36
- value = yield
37
- return value or new.mode.rank > existing.mode.rank
38
-
39
-
40
- @Locator.static_hooks.on_input
41
- def standardize_input_classes[T](
42
- *_: Any,
43
- **__: Any,
44
- ) -> HookGenerator[Iterable[InputType[T]]]:
45
- classes = yield
46
- return tuple(standardize_types(*classes, with_origin=True))
47
-
48
-
49
- @Locator.static_hooks.on_update
50
- def standardize_classes[T](*_: Any, **__: Any) -> HookGenerator[Updater[T]]:
51
- updater = yield
52
- updater.classes = frozenset(standardize_types(*updater.classes))
53
- return updater
@@ -1,106 +0,0 @@
1
- import itertools
2
- from collections.abc import Callable, Generator, Iterator
3
- from dataclasses import dataclass, field
4
- from inspect import isclass, isgeneratorfunction
5
- from typing import Any, Self, TypeGuard
6
-
7
- from injection.exceptions import HookError
8
-
9
- type HookGenerator[T] = Generator[None, T, T]
10
- type HookGeneratorFunction[**P, T] = Callable[P, HookGenerator[T]]
11
- type HookFunction[**P, T] = Callable[P, T] | HookGeneratorFunction[P, T]
12
-
13
-
14
- @dataclass(eq=False, frozen=True, slots=True)
15
- class Hook[**P, T]:
16
- __functions: list[HookFunction[P, T]] = field(
17
- default_factory=list,
18
- init=False,
19
- repr=False,
20
- )
21
-
22
- def __call__(
23
- self,
24
- wrapped: HookFunction[P, T] | type[HookFunction[P, T]] | None = None,
25
- /,
26
- ) -> Any:
27
- def decorator(wp: Any) -> Any:
28
- self.add(wp() if isclass(wp) else wp)
29
- return wp
30
-
31
- return decorator(wrapped) if wrapped else decorator
32
-
33
- @property
34
- def __stack(self) -> Iterator[HookFunction[P, T]]:
35
- return iter(self.__functions)
36
-
37
- def add(self, *functions: HookFunction[P, T]) -> Self:
38
- self.__functions.extend(reversed(functions))
39
- return self
40
-
41
- @classmethod
42
- def apply_several(cls, handler: Callable[P, T], *hooks: Self) -> Callable[P, T]:
43
- stack = itertools.chain.from_iterable((hook.__stack for hook in hooks))
44
- return cls.__apply_stack(handler, stack)
45
-
46
- @classmethod
47
- def __apply_function(
48
- cls,
49
- handler: Callable[P, T],
50
- function: HookFunction[P, T],
51
- ) -> Callable[P, T]:
52
- if not cls.__is_hook_generator_function(function):
53
- return function # type: ignore[return-value]
54
-
55
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
56
- hook: HookGenerator[T] = function(*args, **kwargs)
57
-
58
- try:
59
- next(hook)
60
-
61
- try:
62
- value = handler(*args, **kwargs)
63
- except BaseException as exc:
64
- hook.throw(exc)
65
- else:
66
- hook.send(value)
67
-
68
- except StopIteration as stop:
69
- return stop.value
70
-
71
- finally:
72
- hook.close()
73
-
74
- raise HookError("Missing return value.")
75
-
76
- return wrapper
77
-
78
- @classmethod
79
- def __apply_stack(
80
- cls,
81
- handler: Callable[P, T],
82
- stack: Iterator[HookFunction[P, T]],
83
- ) -> Callable[P, T]:
84
- for function in stack:
85
- new_handler = cls.__apply_function(handler, function)
86
- return cls.__apply_stack(new_handler, stack)
87
-
88
- return handler
89
-
90
- @staticmethod
91
- def __is_hook_generator_function[**_P, _T](
92
- function: HookFunction[_P, _T],
93
- ) -> TypeGuard[HookGeneratorFunction[_P, _T]]:
94
- for fn in function, getattr(function, "__call__", None):
95
- if isgeneratorfunction(fn):
96
- return True
97
-
98
- return False
99
-
100
-
101
- def apply_hooks[**P, T](
102
- handler: Callable[P, T],
103
- hook: Hook[P, T],
104
- *hooks: Hook[P, T],
105
- ) -> Callable[P, T]:
106
- return Hook.apply_several(handler, hook, *hooks)