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
2 changes: 1 addition & 1 deletion documentation/entrypoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What is it?

_An entrypoint is the first function executed when a software component starts._
_An entrypoint is the first function executed when software starts._

When using `python-injection`, you often need to perform several setup actions at the entrypoint _(such as injecting
dependencies, opening a scope, or importing Python modules)_.
Expand Down
2 changes: 0 additions & 2 deletions injection/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,6 @@ class Module:
Function to unlock the module by deleting cached instances of singletons.
"""

@contextmanager
def load_profile(self, *names: str) -> Iterator[Self]: ...
async def all_ready(self) -> None: ...
def add_logger(self, logger: Logger) -> Self: ...
@classmethod
Expand Down
11 changes: 0 additions & 11 deletions injection/_core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,17 +817,6 @@ def unsafe_unlocking(self) -> None:
for broker in self.__brokers:
broker.unsafe_unlocking()

def load_profile(self, *names: str) -> ContextManager[Self]:
modules = (self.from_name(name) for name in names)
self.unlock().init_modules(*modules)

@contextmanager
def unload() -> Iterator[Self]:
yield self
self.unlock().init_modules()

return unload()

async def all_ready(self) -> None:
for broker in self.__brokers:
await broker.all_ready()
Expand Down
39 changes: 22 additions & 17 deletions injection/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from types import ModuleType as PythonModule
from typing import Any, Self, final, overload

from injection import Module, mod
from injection.loaders import PythonModuleLoader
from injection import Module
from injection.loaders import ProfileLoader, PythonModuleLoader

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

Expand All @@ -35,7 +35,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
wrapped: EntrypointSetupMethod[*Ts, P, T1, T2],
/,
*,
module: Module = ...,
profile_loader: ProfileLoader = ...,
) -> EntrypointDecorator[P, T1, T2]: ...


Expand All @@ -44,7 +44,7 @@ def entrypointmaker[*Ts, **P, T1, T2](
wrapped: None = ...,
/,
*,
module: Module = ...,
profile_loader: ProfileLoader = ...,
) -> Callable[
[EntrypointSetupMethod[*Ts, P, T1, T2]],
EntrypointDecorator[P, T1, T2],
Expand All @@ -55,12 +55,12 @@ def entrypointmaker[*Ts, **P, T1, T2](
wrapped: EntrypointSetupMethod[*Ts, P, T1, T2] | None = None,
/,
*,
module: Module | None = None,
profile_loader: ProfileLoader | None = None,
) -> Any:
def decorator(
wp: EntrypointSetupMethod[*Ts, P, T1, T2],
) -> EntrypointDecorator[P, T1, T2]:
return Entrypoint._make_decorator(wp, module)
return Entrypoint._make_decorator(wp, profile_loader)

return decorator(wrapped) if wrapped else decorator

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

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

@property
def __module(self) -> Module:
return self.profile_loader.module

def async_to_sync[_T](
self: AsyncEntrypoint[P, _T],
run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
Expand All @@ -95,7 +99,7 @@ def decorate(
return self.__recreate(decorator(self.function))

def inject(self) -> Self:
return self.decorate(self.module.make_injected_function)
return self.decorate(self.__module.make_injected_function)

def load_modules(
self,
Expand All @@ -105,13 +109,13 @@ def load_modules(
) -> Self:
return self.setup(lambda: loader.load(*packages))

def load_profile(self, /, *names: str) -> Self:
def load_profile(self, name: str, /) -> Self:
@contextmanager
def decorator(module: Module) -> Iterator[None]:
with module.load_profile(*names):
def decorator(loader: ProfileLoader) -> Iterator[None]:
with loader.load(name):
yield

return self.decorate(decorator(self.module))
return self.decorate(decorator(self.profile_loader))

def setup(self, function: Callable[..., Any], /) -> Self:
@contextmanager
Expand All @@ -136,20 +140,21 @@ def __recreate[**_P, _T](
function: Callable[_P, _T],
/,
) -> Entrypoint[_P, _T]:
return type(self)(function, self.module)
return type(self)(function, self.profile_loader)

@classmethod
def _make_decorator[*Ts, _T](
cls,
setup_method: EntrypointSetupMethod[*Ts, P, T, _T],
/,
module: Module | None = None,
profile_loader: ProfileLoader | None = None,
) -> EntrypointDecorator[P, T, _T]:
module = module or mod()
setup_method = module.make_injected_function(setup_method)
profile_loader = profile_loader or ProfileLoader()
setup_method = profile_loader.module.make_injected_function(setup_method)

def decorator(function: Callable[P, T]) -> Callable[P, _T]:
self = cls(function, module)
profile_loader.init()
Comment thread
remimd marked this conversation as resolved.
self = cls(function, profile_loader)
return MethodType(setup_method, self)().function

return decorator
112 changes: 98 additions & 14 deletions injection/loaders.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
from __future__ import annotations

import itertools
import sys
from collections.abc import Callable, Iterator, Mapping
from abc import abstractmethod
from collections.abc import Callable, Iterator, Mapping, Sequence
from dataclasses import dataclass, field
from importlib import import_module
from importlib.util import find_spec
from os.path import isfile
from pkgutil import walk_packages
from types import MappingProxyType
from types import MappingProxyType, TracebackType
from types import ModuleType as PythonModule
from typing import ClassVar, ContextManager, Self

from injection import Module, mod

__all__ = ("PythonModuleLoader", "load_packages", "load_profile")
from typing import ClassVar, Protocol, Self, runtime_checkable

from injection import Module, Priority, mod

def load_profile(*names: str) -> ContextManager[Module]:
"""
Injection module initialization function based on profile name.
A profile name is equivalent to an injection module name.
"""

return mod().load_profile(*names)
__all__ = (
"LoadedProfile",
"ProfileLoader",
"PythonModuleLoader",
"load_packages",
"load_profile",
)


def load_packages(
Expand All @@ -36,6 +36,15 @@ def load_packages(
return PythonModuleLoader(predicate).load(*packages).modules


def load_profile(name: str, /, loader: ProfileLoader | None = None) -> LoadedProfile:
"""
Injection module initialization function based on a profile name.
A profile name is equivalent to an injection module name.
"""

return (loader or ProfileLoader()).load(name)


@dataclass(repr=False, eq=False, frozen=True, slots=True)
class PythonModuleLoader:
predicate: Callable[[str], bool]
Expand Down Expand Up @@ -128,3 +137,78 @@ def predicate(module_name: str) -> bool:
return any(script_name.endswith(suffix) for suffix in suffixes)

return cls(predicate)


@dataclass(repr=False, eq=False, frozen=True, slots=True)
class ProfileLoader:
dependencies: Mapping[str, Sequence[str]] = field(default=MappingProxyType({}))
module: Module = field(default_factory=mod, kw_only=True)
__initialized_modules: set[str] = field(default_factory=set, init=False)

def init(self) -> Self:
self.__init_module_dependencies(self.module)
return self

def load(self, name: str, /) -> LoadedProfile:
self.init()
target_module = self.__init_module_dependencies(mod(name))
self.module.use(target_module, priority=Priority.HIGH)
return _UserLoadedProfile(self, name)

def _unload(self, name: str, /) -> None:
self.module.unlock().stop_using(mod(name))

def __init_module_dependencies(self, module: Module) -> Module:
if not self.__is_initialized(module):
target_modules = tuple(
self.__init_module_dependencies(mod(profile_name))
for profile_name in self.dependencies.get(module.name, ())
)
module.unlock().init_modules(*target_modules)
self.__mark_initialized(module)

return module

def __is_initialized(self, module: Module) -> bool:
return module.name in self.__initialized_modules

def __mark_initialized(self, module: Module) -> None:
self.__initialized_modules.add(module.name)


@runtime_checkable
class LoadedProfile(Protocol):
__slots__ = ()

def __enter__(self) -> Self:
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.unload()

@abstractmethod
def reload(self) -> Self:
raise NotImplementedError

@abstractmethod
def unload(self) -> Self:
raise NotImplementedError


@dataclass(repr=False, eq=False, frozen=True, slots=True)
class _UserLoadedProfile(LoadedProfile):
loader: ProfileLoader
name: str

def reload(self) -> Self:
self.loader.load(self.name)
return self

def unload(self) -> Self:
self.loader._unload(self.name)
return self
10 changes: 5 additions & 5 deletions injection/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import ContextManager, Final
from typing import Final

from injection import Module, mod
from injection.loaders import load_profile
from injection import mod
from injection.loaders import LoadedProfile, ProfileLoader, load_profile

__all__ = (
"load_test_profile",
Expand All @@ -25,5 +25,5 @@
test_singleton = mod(_TEST_PROFILE_NAME).singleton


def load_test_profile(*names: str) -> ContextManager[Module]:
return load_profile(_TEST_PROFILE_NAME, *names)
def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
return load_profile(_TEST_PROFILE_NAME, loader)
7 changes: 4 additions & 3 deletions injection/testing/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import ContextManager, Final
from typing import Final

from injection import Module
from injection.loaders import LoadedProfile, ProfileLoader

__MODULE: Final[Module] = ...

Expand All @@ -12,7 +13,7 @@ test_injectable = __MODULE.injectable
test_scoped = __MODULE.scoped
test_singleton = __MODULE.singleton

def load_test_profile(*names: str) -> ContextManager[Module]:
def load_test_profile(loader: ProfileLoader = ...) -> LoadedProfile:
"""
Context manager or decorator for temporary use test module.
Context manager for temporary use test module.
"""
73 changes: 73 additions & 0 deletions tests/loaders/test_profile_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from dataclasses import dataclass
from uuid import uuid4

import pytest

from injection import find_instance, injectable, mod
from injection.loaders import ProfileLoader


class TestProfileLoader:
def test_load_with_success(self):
profile_name = "test"
global_profile_name = uuid4().hex

@mod(global_profile_name).constant
class GlobalConfig: ...

@injectable
@dataclass
class A:
config: GlobalConfig

@mod(profile_name).injectable(on=A)
class B(A): ...

loader = ProfileLoader(
{
mod().name: [global_profile_name],
profile_name: [global_profile_name],
}
)

with pytest.raises(TypeError):
find_instance(A)

loader.init()

assert type(find_instance(A)) is A
loaded_profile = loader.load(profile_name)
assert type(find_instance(A)) is B

# Cleaning
loaded_profile.unload()

def test_load_with_context_manager(self):
profile_name = "test"
global_profile_name = uuid4().hex

@mod(global_profile_name).constant
class GlobalConfig: ...

@injectable
@dataclass
class A:
config: GlobalConfig

@mod(profile_name).injectable(on=A)
class B(A): ...

loader = ProfileLoader(
{
mod().name: [global_profile_name],
profile_name: [global_profile_name],
}
)
loader.init()

assert type(find_instance(A)) is A

with loader.load(profile_name):
assert type(find_instance(A)) is B

assert type(find_instance(A)) is A
Loading