python-injection 0.18.4__py3-none-any.whl → 0.18.6__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.
injection/__init__.pyi CHANGED
@@ -343,8 +343,6 @@ class Module:
343
343
  Function to unlock the module by deleting cached instances of singletons.
344
344
  """
345
345
 
346
- @contextmanager
347
- def load_profile(self, *names: str) -> Iterator[Self]: ...
348
346
  async def all_ready(self) -> None: ...
349
347
  def add_logger(self, logger: Logger) -> Self: ...
350
348
  @classmethod
injection/_core/module.py CHANGED
@@ -156,6 +156,12 @@ class ModulePriorityUpdated(ModuleEvent):
156
156
  )
157
157
 
158
158
 
159
+ @dataclass(frozen=True, slots=True)
160
+ class UnlockCalled(Event):
161
+ def __str__(self) -> str:
162
+ return "An `unlock` method has been called."
163
+
164
+
159
165
  """
160
166
  Broker
161
167
  """
@@ -179,7 +185,7 @@ class Broker(Protocol):
179
185
  raise NotImplementedError
180
186
 
181
187
  @abstractmethod
182
- def unlock(self) -> Self:
188
+ def unsafe_unlocking(self) -> None:
183
189
  raise NotImplementedError
184
190
 
185
191
  @abstractmethod
@@ -289,12 +295,10 @@ class Locator(Broker):
289
295
 
290
296
  return self
291
297
 
292
- def unlock(self) -> Self:
298
+ def unsafe_unlocking(self) -> None:
293
299
  for injectable in self.__injectables:
294
300
  injectable.unlock()
295
301
 
296
- return self
297
-
298
302
  async def all_ready(self) -> None:
299
303
  for injectable in self.__injectables:
300
304
  if injectable.is_locked:
@@ -802,21 +806,16 @@ class Module(Broker, EventListener):
802
806
  return self
803
807
 
804
808
  def unlock(self) -> Self:
805
- for broker in self.__brokers:
806
- broker.unlock()
807
-
808
- return self
809
+ event = UnlockCalled()
809
810
 
810
- def load_profile(self, *names: str) -> ContextManager[Self]:
811
- modules = (self.from_name(name) for name in names)
812
- self.unlock().init_modules(*modules)
811
+ with self.dispatch(event, lock_bypass=True):
812
+ self.unsafe_unlocking()
813
813
 
814
- @contextmanager
815
- def unload() -> Iterator[Self]:
816
- yield self
817
- self.unlock().init_modules()
814
+ return self
818
815
 
819
- return unload()
816
+ def unsafe_unlocking(self) -> None:
817
+ for broker in self.__brokers:
818
+ broker.unsafe_unlocking()
820
819
 
821
820
  async def all_ready(self) -> None:
822
821
  for broker in self.__brokers:
@@ -839,8 +838,9 @@ class Module(Broker, EventListener):
839
838
  return self.dispatch(self_event)
840
839
 
841
840
  @contextmanager
842
- def dispatch(self, event: Event) -> Iterator[None]:
843
- self.__check_locking()
841
+ def dispatch(self, event: Event, *, lock_bypass: bool = False) -> Iterator[None]:
842
+ if not lock_bypass:
843
+ self.__check_locking()
844
844
 
845
845
  with self.__channel.dispatch(event):
846
846
  try:
injection/entrypoint.py CHANGED
@@ -9,8 +9,8 @@ from types import MethodType
9
9
  from types import ModuleType as PythonModule
10
10
  from typing import Any, Self, final, overload
11
11
 
12
- from injection import Module, mod
13
- from injection.loaders import PythonModuleLoader
12
+ from injection import Module
13
+ from injection.loaders import ProfileLoader, PythonModuleLoader
14
14
 
15
15
  __all__ = ("AsyncEntrypoint", "Entrypoint", "autocall", "entrypointmaker")
16
16
 
@@ -35,7 +35,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
35
35
  wrapped: EntrypointSetupMethod[*Ts, P, T1, T2],
36
36
  /,
37
37
  *,
38
- module: Module = ...,
38
+ profile_loader: ProfileLoader = ...,
39
39
  ) -> EntrypointDecorator[P, T1, T2]: ...
40
40
 
41
41
 
@@ -44,7 +44,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
44
44
  wrapped: None = ...,
45
45
  /,
46
46
  *,
47
- module: Module = ...,
47
+ profile_loader: ProfileLoader = ...,
48
48
  ) -> Callable[
49
49
  [EntrypointSetupMethod[*Ts, P, T1, T2]],
50
50
  EntrypointDecorator[P, T1, T2],
@@ -55,12 +55,12 @@ def entrypointmaker[*Ts, **P, T1, T2](
55
55
  wrapped: EntrypointSetupMethod[*Ts, P, T1, T2] | None = None,
56
56
  /,
57
57
  *,
58
- module: Module | None = None,
58
+ profile_loader: ProfileLoader | None = None,
59
59
  ) -> Any:
60
60
  def decorator(
61
61
  wp: EntrypointSetupMethod[*Ts, P, T1, T2],
62
62
  ) -> EntrypointDecorator[P, T1, T2]:
63
- return Entrypoint._make_decorator(wp, module)
63
+ return Entrypoint._make_decorator(wp, profile_loader)
64
64
 
65
65
  return decorator(wrapped) if wrapped else decorator
66
66
 
@@ -69,11 +69,15 @@ def entrypointmaker[*Ts, **P, T1, T2](
69
69
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
70
70
  class Entrypoint[**P, T]:
71
71
  function: Callable[P, T]
72
- module: Module = field(default_factory=mod)
72
+ profile_loader: ProfileLoader = field(default_factory=ProfileLoader)
73
73
 
74
74
  def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
75
75
  return self.function(*args, **kwargs)
76
76
 
77
+ @property
78
+ def __module(self) -> Module:
79
+ return self.profile_loader.module
80
+
77
81
  def async_to_sync[_T](
78
82
  self: AsyncEntrypoint[P, _T],
79
83
  run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
@@ -95,7 +99,7 @@ class Entrypoint[**P, T]:
95
99
  return self.__recreate(decorator(self.function))
96
100
 
97
101
  def inject(self) -> Self:
98
- return self.decorate(self.module.make_injected_function)
102
+ return self.decorate(self.__module.make_injected_function)
99
103
 
100
104
  def load_modules(
101
105
  self,
@@ -105,13 +109,13 @@ class Entrypoint[**P, T]:
105
109
  ) -> Self:
106
110
  return self.setup(lambda: loader.load(*packages))
107
111
 
108
- def load_profile(self, /, *names: str) -> Self:
112
+ def load_profile(self, name: str, /) -> Self:
109
113
  @contextmanager
110
- def decorator(module: Module) -> Iterator[None]:
111
- with module.load_profile(*names):
114
+ def decorator(loader: ProfileLoader) -> Iterator[None]:
115
+ with loader.load(name):
112
116
  yield
113
117
 
114
- return self.decorate(decorator(self.module))
118
+ return self.decorate(decorator(self.profile_loader))
115
119
 
116
120
  def setup(self, function: Callable[..., Any], /) -> Self:
117
121
  @contextmanager
@@ -136,20 +140,21 @@ class Entrypoint[**P, T]:
136
140
  function: Callable[_P, _T],
137
141
  /,
138
142
  ) -> Entrypoint[_P, _T]:
139
- return type(self)(function, self.module)
143
+ return type(self)(function, self.profile_loader)
140
144
 
141
145
  @classmethod
142
146
  def _make_decorator[*Ts, _T](
143
147
  cls,
144
148
  setup_method: EntrypointSetupMethod[*Ts, P, T, _T],
145
149
  /,
146
- module: Module | None = None,
150
+ profile_loader: ProfileLoader | None = None,
147
151
  ) -> EntrypointDecorator[P, T, _T]:
148
- module = module or mod()
149
- setup_method = module.make_injected_function(setup_method)
152
+ profile_loader = profile_loader or ProfileLoader()
153
+ setup_method = profile_loader.module.make_injected_function(setup_method)
150
154
 
151
155
  def decorator(function: Callable[P, T]) -> Callable[P, _T]:
152
- self = cls(function, module)
156
+ profile_loader.init()
157
+ self = cls(function, profile_loader)
153
158
  return MethodType(setup_method, self)().function
154
159
 
155
160
  return decorator
injection/loaders.py CHANGED
@@ -1,27 +1,27 @@
1
+ from __future__ import annotations
2
+
1
3
  import itertools
2
4
  import sys
3
- from collections.abc import Callable, Iterator, Mapping
5
+ from abc import abstractmethod
6
+ from collections.abc import Callable, Iterator, Mapping, Sequence
4
7
  from dataclasses import dataclass, field
5
8
  from importlib import import_module
6
9
  from importlib.util import find_spec
7
10
  from os.path import isfile
8
11
  from pkgutil import walk_packages
9
- from types import MappingProxyType
12
+ from types import MappingProxyType, TracebackType
10
13
  from types import ModuleType as PythonModule
11
- from typing import ClassVar, ContextManager, Self
12
-
13
- from injection import Module, mod
14
-
15
- __all__ = ("PythonModuleLoader", "load_packages", "load_profile")
14
+ from typing import ClassVar, Protocol, Self, runtime_checkable
16
15
 
16
+ from injection import Module, Priority, mod
17
17
 
18
- def load_profile(*names: str) -> ContextManager[Module]:
19
- """
20
- Injection module initialization function based on profile name.
21
- A profile name is equivalent to an injection module name.
22
- """
23
-
24
- return mod().load_profile(*names)
18
+ __all__ = (
19
+ "LoadedProfile",
20
+ "ProfileLoader",
21
+ "PythonModuleLoader",
22
+ "load_packages",
23
+ "load_profile",
24
+ )
25
25
 
26
26
 
27
27
  def load_packages(
@@ -36,6 +36,15 @@ def load_packages(
36
36
  return PythonModuleLoader(predicate).load(*packages).modules
37
37
 
38
38
 
39
+ def load_profile(name: str, /, loader: ProfileLoader | None = None) -> LoadedProfile:
40
+ """
41
+ Injection module initialization function based on a profile name.
42
+ A profile name is equivalent to an injection module name.
43
+ """
44
+
45
+ return (loader or ProfileLoader()).load(name)
46
+
47
+
39
48
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
40
49
  class PythonModuleLoader:
41
50
  predicate: Callable[[str], bool]
@@ -128,3 +137,78 @@ class PythonModuleLoader:
128
137
  return any(script_name.endswith(suffix) for suffix in suffixes)
129
138
 
130
139
  return cls(predicate)
140
+
141
+
142
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
143
+ class ProfileLoader:
144
+ dependencies: Mapping[str, Sequence[str]] = field(default=MappingProxyType({}))
145
+ module: Module = field(default_factory=mod, kw_only=True)
146
+ __initialized_modules: set[str] = field(default_factory=set, init=False)
147
+
148
+ def init(self) -> Self:
149
+ self.__init_module_dependencies(self.module)
150
+ return self
151
+
152
+ def load(self, name: str, /) -> LoadedProfile:
153
+ self.init()
154
+ target_module = self.__init_module_dependencies(mod(name))
155
+ self.module.use(target_module, priority=Priority.HIGH)
156
+ return _UserLoadedProfile(self, name)
157
+
158
+ def _unload(self, name: str, /) -> None:
159
+ self.module.unlock().stop_using(mod(name))
160
+
161
+ def __init_module_dependencies(self, module: Module) -> Module:
162
+ if not self.__is_initialized(module):
163
+ target_modules = tuple(
164
+ self.__init_module_dependencies(mod(profile_name))
165
+ for profile_name in self.dependencies.get(module.name, ())
166
+ )
167
+ module.unlock().init_modules(*target_modules)
168
+ self.__mark_initialized(module)
169
+
170
+ return module
171
+
172
+ def __is_initialized(self, module: Module) -> bool:
173
+ return module.name in self.__initialized_modules
174
+
175
+ def __mark_initialized(self, module: Module) -> None:
176
+ self.__initialized_modules.add(module.name)
177
+
178
+
179
+ @runtime_checkable
180
+ class LoadedProfile(Protocol):
181
+ __slots__ = ()
182
+
183
+ def __enter__(self) -> Self:
184
+ return self
185
+
186
+ def __exit__(
187
+ self,
188
+ exc_type: type[BaseException] | None,
189
+ exc_value: BaseException | None,
190
+ traceback: TracebackType | None,
191
+ ) -> None:
192
+ self.unload()
193
+
194
+ @abstractmethod
195
+ def reload(self) -> Self:
196
+ raise NotImplementedError
197
+
198
+ @abstractmethod
199
+ def unload(self) -> Self:
200
+ raise NotImplementedError
201
+
202
+
203
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
204
+ class _UserLoadedProfile(LoadedProfile):
205
+ loader: ProfileLoader
206
+ name: str
207
+
208
+ def reload(self) -> Self:
209
+ self.loader.load(self.name)
210
+ return self
211
+
212
+ def unload(self) -> Self:
213
+ self.loader._unload(self.name)
214
+ return self
@@ -1,7 +1,7 @@
1
- from typing import ContextManager, Final
1
+ from typing import Final
2
2
 
3
- from injection import Module, mod
4
- from injection.loaders import load_profile
3
+ from injection import mod
4
+ from injection.loaders import LoadedProfile, ProfileLoader, load_profile
5
5
 
6
6
  __all__ = (
7
7
  "load_test_profile",
@@ -25,5 +25,5 @@ test_scoped = mod(_TEST_PROFILE_NAME).scoped
25
25
  test_singleton = mod(_TEST_PROFILE_NAME).singleton
26
26
 
27
27
 
28
- def load_test_profile(*names: str) -> ContextManager[Module]:
29
- return load_profile(_TEST_PROFILE_NAME, *names)
28
+ def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
29
+ return load_profile(_TEST_PROFILE_NAME, loader)
@@ -1,6 +1,7 @@
1
- from typing import ContextManager, Final
1
+ from typing import Final
2
2
 
3
3
  from injection import Module
4
+ from injection.loaders import LoadedProfile, ProfileLoader
4
5
 
5
6
  __MODULE: Final[Module] = ...
6
7
 
@@ -12,7 +13,7 @@ test_injectable = __MODULE.injectable
12
13
  test_scoped = __MODULE.scoped
13
14
  test_singleton = __MODULE.singleton
14
15
 
15
- def load_test_profile(*names: str) -> ContextManager[Module]:
16
+ def load_test_profile(loader: ProfileLoader = ...) -> LoadedProfile:
16
17
  """
17
- Context manager or decorator for temporary use test module.
18
+ Context manager for temporary use test module.
18
19
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.18.4
3
+ Version: 0.18.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,13 +1,13 @@
1
1
  injection/__init__.py,sha256=7ZRUlO5EEPWO7IlbYHD-8DOX-cg4Np4nYq5fpw-U56o,1259
2
- injection/__init__.pyi,sha256=O2W6ZWcwH58JnDmJNkJ6cdoaO0D2NE7PEVFgfJNTwB0,10717
3
- injection/entrypoint.py,sha256=oTOwU2MXAaATuawADQ4KOPp5Tet2gL4dJD0IdClZ8sY,4461
2
+ injection/__init__.pyi,sha256=33A511o3QKQAiQOHL8OQtFPdF-ObM1BqWPmswXhxj7c,10634
3
+ injection/entrypoint.py,sha256=12b0_zHAFxHCerAoJTIHkhqi3mLkgheECYAaCUZv_DU,4751
4
4
  injection/exceptions.py,sha256=v57yMujiq6H_zwwn30A8UYEZX9R9k-bY8FnsdaimPM4,1025
5
- injection/loaders.py,sha256=8tS3zVvmxKf5E1ZJBTBjZpHkXETxNCL1dq8rbh2vzy4,4075
5
+ injection/loaders.py,sha256=_0V64d45WoT0OVWML8ZQts4gMXCbT7LkCS3KLl6STMI,6509
6
6
  injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  injection/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  injection/_core/descriptors.py,sha256=jH0pyIlPurMmU4yXr-HKS_7BJ-9d0XUvEx4pQre3QeI,704
9
9
  injection/_core/injectables.py,sha256=Rg1nxDkbcpeX4ELohrNVMguPhN36SNQuD0JKfyfL6bI,6192
10
- injection/_core/module.py,sha256=mYc-x7hH-mbminiOFXJWxA7KkBpleuW84IOpzfeKQHc,31874
10
+ injection/_core/module.py,sha256=zpYLLhtXPLb6hE1U_A5AJ-ffffT_aPrq4DuVmJoxz3A,31931
11
11
  injection/_core/scope.py,sha256=OBzVY1mUApryqIZKQtwHz7wiuY13MfouyaHp50DpWeQ,8300
12
12
  injection/_core/slots.py,sha256=g9TG6CbqRzCsjg01iPyfRtTTUCJnnJOwcj9mJabH0dc,37
13
13
  injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -20,9 +20,9 @@ injection/_core/common/type.py,sha256=SCDtmBv9qFvEf5o5tTgCuwMDfuo1fgjSW0bUqA8ACi
20
20
  injection/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  injection/ext/fastapi.py,sha256=layUUer5IWiZX6Mmx1_RCYDLNCtEHtpya5ZL6TTBOkY,968
22
22
  injection/ext/fastapi.pyi,sha256=8OZEUjHFB9n7QXv_dtXdDuXW-r2huQEFsJ03gJOOvwQ,125
23
- injection/testing/__init__.py,sha256=PsrvNxYvn11if8CJpEYxcTxniqMt2w_7fBaDkY9RA5o,898
24
- injection/testing/__init__.pyi,sha256=iOii0i9F5n7znltGeGQYI2KXC_if9SAogLh1h03yx-0,540
25
- python_injection-0.18.4.dist-info/METADATA,sha256=ZECngXj7kg_M2cFQd3uWHJKuRiDbOLY8m97kF8LrZtU,3397
26
- python_injection-0.18.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- python_injection-0.18.4.dist-info/licenses/LICENSE,sha256=oC77BOa9kaaQni5rW-Z-ytz3E5h4EVg248BHg9UFgyg,1063
28
- python_injection-0.18.4.dist-info/RECORD,,
23
+ injection/testing/__init__.py,sha256=bJ7WXBXrw4rHc91AFVFnOwFLWOlpvX9Oh2SnRQ_NESo,919
24
+ injection/testing/__init__.pyi,sha256=raGsGlxwbz3jkzJwA_5oCIE1emWINjT2UuwzbnqRb-0,577
25
+ python_injection-0.18.6.dist-info/METADATA,sha256=J8D8DrVAJfAoGen7LNrtT303UXR5yUT5fYw6P6ufVsQ,3397
26
+ python_injection-0.18.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ python_injection-0.18.6.dist-info/licenses/LICENSE,sha256=oC77BOa9kaaQni5rW-Z-ytz3E5h4EVg248BHg9UFgyg,1063
28
+ python_injection-0.18.6.dist-info/RECORD,,