Skip to content

Commit 0320d25

Browse files
authored
feat: ✨ Inject self
1 parent c8a590b commit 0320d25

File tree

5 files changed

+275
-174
lines changed

5 files changed

+275
-174
lines changed

injection/_pkg.pyi

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,11 @@ class Module:
3939

4040
def __init__(self, name: str = ...): ...
4141
def __contains__(self, cls: type | UnionType, /) -> bool: ...
42-
def inject(
43-
self,
44-
wrapped: Callable[..., Any] = ...,
45-
/,
46-
*,
47-
force: bool = ...,
48-
):
42+
def inject(self, wrapped: Callable[..., Any] = ..., /):
4943
"""
5044
Decorator applicable to a class or function. Inject function dependencies using
5145
parameter type annotations. If applied to a class, the dependencies resolved
5246
will be those of the `__init__` method.
53-
54-
With `force=True`, parameters passed to replace dependencies will be ignored.
5547
"""
5648

5749
def injectable(

injection/common/lazy.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
from collections.abc import Callable, Iterator, Mapping
2+
from contextlib import suppress
23
from types import MappingProxyType
34
from typing import Any, Generic, TypeVar
45

56
from injection.common.tools.threading import thread_lock
67

78
__all__ = ("Lazy", "LazyMapping")
89

9-
_sentinel = object()
10-
1110
_T = TypeVar("_T")
1211
_K = TypeVar("_K")
1312
_V = TypeVar("_V")
1413

1514

1615
class Lazy(Generic[_T]):
17-
__slots__ = ("__factory", "__value")
16+
__slots__ = ("is_set", "__generator")
1817

1918
def __init__(self, factory: Callable[[], _T]):
20-
self.__factory = factory
21-
self.__value = _sentinel
19+
def generator() -> Iterator[_T]:
20+
nonlocal factory
2221

23-
def __invert__(self) -> _T:
24-
if not self.is_set:
2522
with thread_lock:
26-
self.__value = self.__factory()
27-
self.__factory = _sentinel
23+
value = factory()
24+
self.is_set = True
25+
del factory
2826

29-
return self.__value
27+
while True:
28+
yield value
29+
30+
self.is_set = False
31+
self.__generator = generator()
32+
33+
def __invert__(self) -> _T:
34+
return next(self.__generator)
35+
36+
def __call__(self) -> _T:
37+
return ~self
3038

3139
def __setattr__(self, name: str, value: Any, /):
32-
if self.is_set:
33-
raise TypeError(f"`{self}` is frozen.")
40+
with suppress(AttributeError):
41+
if self.is_set:
42+
raise TypeError(f"`{self}` is frozen.")
3443

3544
return super().__setattr__(name, value)
3645

37-
@property
38-
def is_set(self) -> bool:
39-
try:
40-
return self.__factory is _sentinel
41-
except AttributeError:
42-
return False
43-
4446

4547
class LazyMapping(Mapping[_K, _V]):
4648
__slots__ = ("__lazy",)
4749

48-
__lazy: Lazy[MappingProxyType[_K, _V]]
49-
5050
def __init__(self, iterator: Iterator[tuple[_K, _V]]):
5151
self.__lazy = Lazy(lambda: MappingProxyType(dict(iterator)))
5252

@@ -58,3 +58,7 @@ def __iter__(self) -> Iterator[_K]:
5858

5959
def __len__(self) -> int:
6060
return len(~self.__lazy)
61+
62+
@property
63+
def is_set(self) -> bool:
64+
return self.__lazy.is_set

injection/core/module.py

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@
1616
from contextlib import ContextDecorator, contextmanager, suppress
1717
from dataclasses import dataclass, field
1818
from enum import Enum, auto
19-
from functools import partialmethod, singledispatchmethod, wraps
19+
from functools import (
20+
partialmethod,
21+
singledispatchmethod,
22+
update_wrapper,
23+
wraps,
24+
)
2025
from inspect import Signature, isclass
21-
from types import MappingProxyType, UnionType
26+
from types import UnionType
2227
from typing import (
2328
Any,
29+
ClassVar,
2430
ContextManager,
2531
NamedTuple,
2632
NoReturn,
@@ -169,7 +175,7 @@ def get_instance(self) -> _T:
169175
class SingletonInjectable(BaseInjectable[_T]):
170176
__slots__ = ("__dict__",)
171177

172-
__INSTANCE_KEY = "$instance"
178+
__INSTANCE_KEY: ClassVar[str] = "$instance"
173179

174180
@property
175181
def cache(self) -> MutableMapping[str, Any]:
@@ -405,21 +411,15 @@ def inject(
405411
wrapped: Callable[..., Any] = None,
406412
/,
407413
*,
408-
force: bool = False,
409414
return_factory: bool = False,
410415
):
411416
def decorator(wp):
412417
if not return_factory and isclass(wp):
413-
wp.__init__ = self.inject(wp.__init__, force=force)
418+
wp.__init__ = self.inject(wp.__init__)
414419
return wp
415420

416-
lazy_binder = Lazy[Binder](lambda: self.__new_binder(wp))
417-
418-
@wraps(wp)
419-
def wrapper(*args, **kwargs):
420-
arguments = (~lazy_binder).bind(args, kwargs, force)
421-
return wp(*arguments.args, **arguments.kwargs)
422-
421+
wrapper = InjectedFunction(wp).update(self)
422+
self.add_listener(wrapper)
423423
return wrapper
424424

425425
return decorator(wrapped) if wrapped else decorator
@@ -535,21 +535,15 @@ def __move_module(self, module: Module, priority: ModulePriority):
535535
f"`{module}` can't be found in the modules used by `{self}`."
536536
) from exc
537537

538-
def __new_binder(self, target: Callable[..., Any]) -> Binder:
539-
signature = inspect.signature(target, eval_str=True)
540-
binder = Binder(signature).update(self)
541-
self.add_listener(binder)
542-
return binder
543-
544538

545539
"""
546-
Binder
540+
InjectedFunction
547541
"""
548542

549543

550544
@dataclass(repr=False, frozen=True, slots=True)
551545
class Dependencies:
552-
mapping: MappingProxyType[str, Injectable]
546+
mapping: Mapping[str, Injectable]
553547

554548
def __bool__(self) -> bool:
555549
return bool(self.mapping)
@@ -558,75 +552,137 @@ def __iter__(self) -> Iterator[tuple[str, Any]]:
558552
for name, injectable in self.mapping.items():
559553
yield name, injectable.get_instance()
560554

555+
@property
556+
def are_resolved(self) -> bool:
557+
if isinstance(self.mapping, LazyMapping) and not self.mapping.is_set:
558+
return False
559+
560+
return bool(self)
561+
561562
@property
562563
def arguments(self) -> OrderedDict[str, Any]:
563564
return OrderedDict(self)
564565

565566
@classmethod
566567
def from_mapping(cls, mapping: Mapping[str, Injectable]):
567-
return cls(mapping=MappingProxyType(mapping))
568+
return cls(mapping=mapping)
568569

569570
@classmethod
570571
def empty(cls):
571572
return cls.from_mapping({})
572573

573574
@classmethod
574-
def resolve(cls, signature: Signature, module: Module):
575-
dependencies = LazyMapping(cls.__resolver(signature, module))
575+
def resolve(cls, signature: Signature, module: Module, owner: type = None):
576+
dependencies = LazyMapping(cls.__resolver(signature, module, owner))
576577
return cls.from_mapping(dependencies)
577578

578579
@classmethod
579580
def __resolver(
580581
cls,
581582
signature: Signature,
582583
module: Module,
584+
owner: type = None,
583585
) -> Iterator[tuple[str, Injectable]]:
584-
for name, parameter in signature.parameters.items():
586+
for name, annotation in cls.__get_annotations(signature, owner):
585587
try:
586-
injectable = module[parameter.annotation]
588+
injectable = module[annotation]
587589
except KeyError:
588590
continue
589591

590592
yield name, injectable
591593

594+
@staticmethod
595+
def __get_annotations(
596+
signature: Signature,
597+
owner: type = None,
598+
) -> Iterator[tuple[str, type | Any]]:
599+
parameters = iter(signature.parameters.items())
600+
601+
if owner:
602+
name, _ = next(parameters)
603+
yield name, owner
604+
605+
for name, parameter in parameters:
606+
yield name, parameter.annotation
607+
592608

593609
class Arguments(NamedTuple):
594610
args: Iterable[Any]
595611
kwargs: Mapping[str, Any]
596612

597613

598-
class Binder(EventListener):
599-
__slots__ = ("__signature", "__dependencies")
614+
class InjectedFunction(EventListener):
615+
__slots__ = ("__dict__", "__wrapper", "__dependencies", "__owner")
616+
617+
def __init__(self, wrapped: Callable[..., Any], /):
618+
update_wrapper(self, wrapped)
619+
self.__signature__ = Lazy[Signature](
620+
lambda: inspect.signature(wrapped, eval_str=True)
621+
)
622+
623+
@wraps(wrapped)
624+
def wrapper(*args, **kwargs):
625+
args, kwargs = self.bind(args, kwargs)
626+
return wrapped(*args, **kwargs)
600627

601-
def __init__(self, signature: Signature):
602-
self.__signature = signature
628+
self.__wrapper = wrapper
603629
self.__dependencies = Dependencies.empty()
630+
self.__owner = None
631+
632+
def __repr__(self) -> str:
633+
return repr(self.__wrapper)
634+
635+
def __str__(self) -> str:
636+
return str(self.__wrapper)
637+
638+
def __call__(self, /, *args, **kwargs) -> Any:
639+
return self.__wrapper(*args, **kwargs)
640+
641+
def __get__(self, instance: object | None, owner: type):
642+
if instance is None:
643+
return self
644+
645+
return self.__wrapper.__get__(instance, owner)
646+
647+
def __set_name__(self, owner: type, name: str):
648+
if self.__dependencies.are_resolved:
649+
raise TypeError(
650+
"`__set_name__` is called after dependencies have been resolved."
651+
)
652+
653+
if self.__owner:
654+
raise TypeError("Function owner is already defined.")
655+
656+
self.__owner = owner
657+
658+
@property
659+
def signature(self) -> Signature:
660+
return self.__signature__()
604661

605662
def bind(
606663
self,
607664
args: Iterable[Any] = (),
608665
kwargs: Mapping[str, Any] = None,
609-
force: bool = False,
610666
) -> Arguments:
611667
if kwargs is None:
612668
kwargs = {}
613669

614670
if not self.__dependencies:
615671
return Arguments(args, kwargs)
616672

617-
bound = self.__signature.bind_partial(*args, **kwargs)
673+
bound = self.signature.bind_partial(*args, **kwargs)
618674
dependencies = self.__dependencies.arguments
619-
620-
if force:
621-
bound.arguments |= dependencies
622-
else:
623-
bound.arguments = dependencies | bound.arguments
675+
bound.arguments = dependencies | bound.arguments
624676

625677
return Arguments(bound.args, bound.kwargs)
626678

627679
def update(self, module: Module):
628680
with thread_lock:
629-
self.__dependencies = Dependencies.resolve(self.__signature, module)
681+
self.__dependencies = Dependencies.resolve(
682+
self.signature,
683+
module,
684+
self.__owner,
685+
)
630686

631687
return self
632688

0 commit comments

Comments
 (0)