Skip to content

Commit 7c43173

Browse files
author
remimd
committed
feat: ✨ ProfileLoader
1 parent c55b333 commit 7c43173

8 files changed

Lines changed: 234 additions & 157 deletions

File tree

documentation/entrypoint.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## What is it?
44

5-
_An entrypoint is the first function executed when a software component starts._
5+
_An entrypoint is the first function executed when software starts._
66

77
When using `python-injection`, you often need to perform several setup actions at the entrypoint _(such as injecting
88
dependencies, opening a scope, or importing Python modules)_.

injection/__init__.pyi

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,6 @@ class Module:
343343
Function to unlock the module by deleting cached instances of singletons.
344344
"""
345345

346-
@contextmanager
347-
def load_profile(self, *names: str) -> Iterator[Self]: ...
348346
async def all_ready(self) -> None: ...
349347
def add_logger(self, logger: Logger) -> Self: ...
350348
@classmethod

injection/_core/module.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -817,17 +817,6 @@ def unsafe_unlocking(self) -> None:
817817
for broker in self.__brokers:
818818
broker.unsafe_unlocking()
819819

820-
def load_profile(self, *names: str) -> ContextManager[Self]:
821-
modules = (self.from_name(name) for name in names)
822-
self.unlock().init_modules(*modules)
823-
824-
@contextmanager
825-
def unload() -> Iterator[Self]:
826-
yield self
827-
self.unlock().init_modules()
828-
829-
return unload()
830-
831820
async def all_ready(self) -> None:
832821
for broker in self.__brokers:
833822
await broker.all_ready()

injection/entrypoint.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from types import ModuleType as PythonModule
1010
from typing import Any, Self, final, overload
1111

12-
from injection import Module, mod
13-
from injection.loaders import PythonModuleLoader
12+
from injection import Module
13+
from injection.loaders import ProfileLoader, PythonModuleLoader
1414

1515
__all__ = ("AsyncEntrypoint", "Entrypoint", "autocall", "entrypointmaker")
1616

@@ -35,7 +35,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
3535
wrapped: EntrypointSetupMethod[*Ts, P, T1, T2],
3636
/,
3737
*,
38-
module: Module = ...,
38+
profile_loader: ProfileLoader = ...,
3939
) -> EntrypointDecorator[P, T1, T2]: ...
4040

4141

@@ -44,7 +44,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
4444
wrapped: None = ...,
4545
/,
4646
*,
47-
module: Module = ...,
47+
profile_loader: ProfileLoader = ...,
4848
) -> Callable[
4949
[EntrypointSetupMethod[*Ts, P, T1, T2]],
5050
EntrypointDecorator[P, T1, T2],
@@ -55,12 +55,12 @@ def entrypointmaker[*Ts, **P, T1, T2](
5555
wrapped: EntrypointSetupMethod[*Ts, P, T1, T2] | None = None,
5656
/,
5757
*,
58-
module: Module | None = None,
58+
profile_loader: ProfileLoader | None = None,
5959
) -> Any:
6060
def decorator(
6161
wp: EntrypointSetupMethod[*Ts, P, T1, T2],
6262
) -> EntrypointDecorator[P, T1, T2]:
63-
return Entrypoint._make_decorator(wp, module)
63+
return Entrypoint._make_decorator(wp, profile_loader)
6464

6565
return decorator(wrapped) if wrapped else decorator
6666

@@ -69,11 +69,15 @@ def decorator(
6969
@dataclass(repr=False, eq=False, frozen=True, slots=True)
7070
class Entrypoint[**P, T]:
7171
function: Callable[P, T]
72-
module: Module = field(default_factory=mod)
72+
profile_loader: ProfileLoader = field(default_factory=ProfileLoader, kw_only=True)
7373

7474
def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
7575
return self.function(*args, **kwargs)
7676

77+
@property
78+
def _module(self) -> Module:
79+
return self.profile_loader.module
80+
7781
def async_to_sync[_T](
7882
self: AsyncEntrypoint[P, _T],
7983
run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
@@ -95,7 +99,7 @@ def decorate(
9599
return self.__recreate(decorator(self.function))
96100

97101
def inject(self) -> Self:
98-
return self.decorate(self.module.make_injected_function)
102+
return self.decorate(self._module.make_injected_function)
99103

100104
def load_modules(
101105
self,
@@ -105,13 +109,13 @@ def load_modules(
105109
) -> Self:
106110
return self.setup(lambda: loader.load(*packages))
107111

108-
def load_profile(self, /, *names: str) -> Self:
112+
def load_profile(self, name: str, /) -> Self:
109113
@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):
112116
yield
113117

114-
return self.decorate(decorator(self.module))
118+
return self.decorate(decorator(self.profile_loader))
115119

116120
def setup(self, function: Callable[..., Any], /) -> Self:
117121
@contextmanager
@@ -136,20 +140,21 @@ def __recreate[**_P, _T](
136140
function: Callable[_P, _T],
137141
/,
138142
) -> Entrypoint[_P, _T]:
139-
return type(self)(function, self.module)
143+
return type(self)(function, profile_loader=self.profile_loader)
140144

141145
@classmethod
142146
def _make_decorator[*Ts, _T](
143147
cls,
144148
setup_method: EntrypointSetupMethod[*Ts, P, T, _T],
145149
/,
146-
module: Module | None = None,
150+
profile_loader: ProfileLoader | None = None,
147151
) -> 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)
150154

151155
def decorator(function: Callable[P, T]) -> Callable[P, _T]:
152-
self = cls(function, module)
156+
profile_loader.init()
157+
self = cls(function, profile_loader=profile_loader)
153158
return MethodType(setup_method, self)().function
154159

155160
return decorator

injection/loaders.py

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1+
from __future__ import annotations
2+
13
import itertools
24
import sys
3-
from collections.abc import Callable, Iterator, Mapping
5+
from abc import abstractmethod
6+
from collections.abc import Callable, Iterator, Mapping, Sequence
47
from dataclasses import dataclass, field
58
from importlib import import_module
69
from importlib.util import find_spec
710
from os.path import isfile
811
from pkgutil import walk_packages
9-
from types import MappingProxyType
12+
from types import MappingProxyType, TracebackType
1013
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
1615

16+
from injection import Module, Priority, mod
1717

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+
)
2525

2626

2727
def load_packages(
@@ -36,6 +36,15 @@ def load_packages(
3636
return PythonModuleLoader(predicate).load(*packages).modules
3737

3838

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+
3948
@dataclass(repr=False, eq=False, frozen=True, slots=True)
4049
class PythonModuleLoader:
4150
predicate: Callable[[str], bool]
@@ -128,3 +137,78 @@ def predicate(module_name: str) -> bool:
128137
return any(script_name.endswith(suffix) for suffix in suffixes)
129138

130139
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

injection/testing/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import ContextManager, Final
1+
from typing import Final
22

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
55

66
__all__ = (
77
"load_test_profile",
@@ -25,5 +25,5 @@
2525
test_singleton = mod(_TEST_PROFILE_NAME).singleton
2626

2727

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)

injection/testing/__init__.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import ContextManager, Final
1+
from typing import Final
22

33
from injection import Module
4+
from injection.loaders import LoadedProfile, ProfileLoader
45

56
__MODULE: Final[Module] = ...
67

@@ -12,7 +13,7 @@ test_injectable = __MODULE.injectable
1213
test_scoped = __MODULE.scoped
1314
test_singleton = __MODULE.singleton
1415

15-
def load_test_profile(*names: str) -> ContextManager[Module]:
16+
def load_test_profile(loader: ProfileLoader = ...) -> LoadedProfile:
1617
"""
17-
Context manager or decorator for temporary use test module.
18+
Context manager for temporary use test module.
1819
"""

0 commit comments

Comments
 (0)