Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions documentation/loaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,43 @@ def main(profile_name: str = None, /):
if __name__ == "__main__":
main("dev") # One could imagine the profile name being transmitted via an environment variable or CLI parameter
```

## ProfileLoader

_This is a slightly more complete version of `load_profile`._

If you use modules as subsets of dependencies, this class will make your life easier.

It is recommended to use a single instance of `ProfileLoader` to avoid unexpected behavior. If it exists, this instance
must be passed to `entrypointmaker` and `load_test_profile`.

Here's an example of its use:

```python
from injection import mod
from injection.loaders import ProfileLoader

profile_loader = ProfileLoader(
{
mod().name: ["global"],
"dev": ["stub", "global"],
"test": ["stub", "global"],
"stub": ["global"]
}
)

# Ensures that dependent modules are used properly.
# If `init` isn't called, it will be automatically called with `load`.
profile_loader.init()

# Load `dev` profile.
profile_loader.load("dev")
```

> [!NOTE]
> `load` can also be used as a context manager:
>
> ```python
> with profile_loader.load("<profile-name>"):
> ...
> ```
1 change: 1 addition & 0 deletions injection/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class Module:
module: Module,
*,
priority: Priority | PriorityStr = ...,
unlock: bool = ...,
) -> Iterator[Self]:
"""
Context manager or decorator for temporary use of a module.
Expand Down
4 changes: 4 additions & 0 deletions injection/_core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,12 +788,16 @@ def use_temporarily(
module: Module,
*,
priority: Priority | PriorityStr = Priority.get_default(),
unlock: bool = False,
) -> Iterator[Self]:
self.use(module, priority=priority)

try:
yield self
finally:
if unlock:
self.unlock()

self.stop_using(module)

def change_priority(self, module: Module, priority: Priority | PriorityStr) -> Self:
Expand Down
4 changes: 1 addition & 3 deletions injection/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ def _make_decorator[*Ts, _T](
setup_method = profile_loader.module.make_injected_function(setup_method)

def decorator(function: Callable[P, T]) -> Callable[P, _T]:
if profile_loader.module_subsets:
profile_loader.init()

profile_loader.init()
self = cls(function, profile_loader)
return MethodType(setup_method, self)().function

Expand Down
8 changes: 6 additions & 2 deletions injection/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ class ProfileLoader:
module: Module = field(default_factory=mod, kw_only=True)
__initialized_modules: set[str] = field(default_factory=set, init=False)

@property
def __is_empty(self) -> bool:
return not self.module_subsets

def init(self) -> Self:
self.__init_subsets_for(self.module)
return self
Expand All @@ -159,12 +163,12 @@ def _unload(self, name: str, /) -> None:
self.module.unlock().stop_using(mod(name))

def __init_subsets_for(self, module: Module) -> Module:
if not self.__is_initialized(module):
if not self.__is_empty and not self.__is_initialized(module):
target_modules = tuple(
self.__init_subsets_for(mod(name))
for name in self.module_subsets.get(module.name, ())
)
module.unlock().init_modules(*target_modules)
module.init_modules(*target_modules)
self.__mark_initialized(module)

return module
Expand Down
17 changes: 17 additions & 0 deletions tests/core/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,23 @@ def some_function():
some_function()
event_history.assert_length(2)

def test_use_temporarily_with_unlock(self, module):
second_module = Module()

@module.singleton
class A: ...

with pytest.raises(ModuleLockError):
with module.use_temporarily(second_module):
module.find_instance(A)

# Cleaning
module.unlock().stop_using(second_module)

# Ensure there are no errors
with module.use_temporarily(second_module, unlock=True):
module.find_instance(A)

"""
change_priority
"""
Expand Down
Loading