Skip to content

Commit 9376581

Browse files
authored
refactoring: ♻️ Remove decorator classes
1 parent 9dfea98 commit 9376581

File tree

5 files changed

+106
-122
lines changed

5 files changed

+106
-122
lines changed

injection/_pkg.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from .core import Module, ModulePriorities
1+
from .core import Injectable, Module, ModulePriorities
22

33
__all__ = (
4+
"Injectable",
45
"Module",
56
"ModulePriorities",
67
"default_module",
@@ -16,9 +17,7 @@
1617

1718
get_instance = default_module.get_instance
1819
get_lazy_instance = default_module.get_lazy_instance
19-
2020
inject = default_module.inject
2121
injectable = default_module.injectable
22-
singleton = default_module.singleton
23-
2422
set_constant = default_module.set_constant
23+
singleton = default_module.singleton

injection/_pkg.pyi

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
from abc import abstractmethod
12
from collections.abc import Callable, Iterable
23
from contextlib import ContextDecorator
34
from enum import Enum
45
from types import UnionType
5-
from typing import Any, ContextManager, Final, TypeVar, final
6+
from typing import (
7+
Any,
8+
ContextManager,
9+
Final,
10+
Protocol,
11+
TypeVar,
12+
final,
13+
runtime_checkable,
14+
)
615

716
from injection.common.lazy import Lazy
817

@@ -12,12 +21,10 @@ default_module: Final[Module] = ...
1221

1322
get_instance = default_module.get_instance
1423
get_lazy_instance = default_module.get_lazy_instance
15-
1624
inject = default_module.inject
1725
injectable = default_module.injectable
18-
singleton = default_module.singleton
19-
2026
set_constant = default_module.set_constant
27+
singleton = default_module.singleton
2128

2229
@final
2330
class Module:
@@ -42,6 +49,7 @@ class Module:
4249
wrapped: Callable[..., Any] = ...,
4350
/,
4451
*,
52+
cls: type[Injectable] = ...,
4553
on: type | Iterable[type] | UnionType = ...,
4654
):
4755
"""
@@ -119,3 +127,12 @@ class Module:
119127
class ModulePriorities(Enum):
120128
HIGH = ...
121129
LOW = ...
130+
131+
@runtime_checkable
132+
class Injectable(Protocol[_T]):
133+
def __init__(self, factory: Callable[[], _T] = ..., *args, **kwargs): ...
134+
@property
135+
def is_locked(self) -> bool: ...
136+
def unlock(self): ...
137+
@abstractmethod
138+
def get_instance(self) -> _T: ...

injection/common/tools/_type.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from collections.abc import Iterator
1+
from collections.abc import Iterable, Iterator
2+
from inspect import get_annotations, isfunction
23
from types import NoneType, UnionType
34
from typing import Annotated, Any, Union, get_args, get_origin
45

5-
__all__ = ("format_type", "get_origins")
6+
__all__ = ("find_types", "format_type", "get_origins")
67

78

89
def format_type(cls: type | Any) -> str:
@@ -12,25 +13,36 @@ def format_type(cls: type | Any) -> str:
1213
return str(cls)
1314

1415

15-
def get_origins(*classes: type | Any) -> Iterator[type | Any]:
16-
for cls in classes:
17-
origin = get_origin(cls) or cls
16+
def get_origins(*types: type | Any) -> Iterator[type | Any]:
17+
for tp in types:
18+
origin = get_origin(tp) or tp
1819

1920
if origin in (None, NoneType):
2021
continue
2122

22-
arguments = get_args(cls)
23+
elif origin in (Union, UnionType):
24+
args = get_args(tp)
2325

24-
if origin in (Union, UnionType):
25-
yield from get_origins(*arguments)
26+
elif origin is Annotated is not tp:
27+
args = (tp.__origin__,)
28+
29+
else:
30+
yield origin
31+
continue
32+
33+
yield from get_origins(*args)
2634

27-
elif origin is Annotated:
28-
try:
29-
annotated = arguments[0]
30-
except IndexError:
31-
continue
3235

33-
yield from get_origins(annotated)
36+
def find_types(*args: Any) -> Iterator[type | UnionType]:
37+
for argument in args:
38+
if isinstance(argument, Iterable) and not isinstance(argument, type | str):
39+
arguments = argument
40+
41+
elif isfunction(argument):
42+
arguments = (get_annotations(argument, eval_str=True).get("return"),)
3443

3544
else:
36-
yield origin
45+
yield argument
46+
continue
47+
48+
yield from find_types(*arguments)

injection/core/module.py

Lines changed: 54 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from contextlib import ContextDecorator, contextmanager, suppress
1616
from dataclasses import dataclass, field
1717
from enum import Enum, auto
18-
from functools import singledispatchmethod, wraps
19-
from inspect import Signature, get_annotations, isclass, isfunction
18+
from functools import partialmethod, singledispatchmethod, wraps
19+
from inspect import Signature, isclass
2020
from threading import RLock
2121
from types import MappingProxyType, UnionType
2222
from typing import (
@@ -26,13 +26,12 @@
2626
Protocol,
2727
TypeVar,
2828
cast,
29-
final,
3029
runtime_checkable,
3130
)
3231

3332
from injection.common.event import Event, EventChannel, EventListener
3433
from injection.common.lazy import Lazy, LazyMapping
35-
from injection.common.tools import format_type, get_origins
34+
from injection.common.tools import find_types, format_type, get_origins
3635
from injection.exceptions import (
3736
ModuleError,
3837
ModuleLockError,
@@ -133,6 +132,9 @@ def __str__(self) -> str:
133132
class Injectable(Protocol[_T]):
134133
__slots__ = ()
135134

135+
def __init__(self, factory: Callable[[], _T] = ..., *args, **kwargs):
136+
...
137+
136138
@property
137139
def is_locked(self) -> bool:
138140
return False
@@ -288,18 +290,6 @@ def __contains__(self, cls: type | UnionType, /) -> bool:
288290
def __str__(self) -> str:
289291
return self.name or object.__str__(self)
290292

291-
@property
292-
def inject(self) -> InjectDecorator:
293-
return InjectDecorator(self)
294-
295-
@property
296-
def injectable(self) -> InjectableDecorator:
297-
return InjectableDecorator(self, NewInjectable)
298-
299-
@property
300-
def singleton(self) -> InjectableDecorator:
301-
return InjectableDecorator(self, SingletonInjectable)
302-
303293
@property
304294
def is_locked(self) -> bool:
305295
return any(broker.is_locked for broker in self.__brokers)
@@ -309,6 +299,25 @@ def __brokers(self) -> Iterator[Container | Module]:
309299
yield from tuple(self.__modules)
310300
yield self.__container
311301

302+
def injectable(
303+
self,
304+
wrapped: Callable[..., Any] = None,
305+
/,
306+
*,
307+
cls: type[Injectable] = NewInjectable,
308+
on: type | Types = None,
309+
):
310+
def decorator(wp):
311+
factory = self.inject(wp, return_factory=True)
312+
injectable = cls(factory)
313+
classes = find_types(wp, on)
314+
self.update(classes, injectable)
315+
return wp
316+
317+
return decorator(wrapped) if wrapped else decorator
318+
319+
singleton = partialmethod(injectable, cls=SingletonInjectable)
320+
312321
def set_constant(self, instance: _T, on: type | Types = None) -> _T:
313322
cls = type(instance)
314323

@@ -318,6 +327,29 @@ def get_constant():
318327

319328
return instance
320329

330+
def inject(
331+
self,
332+
wrapped: Callable[..., Any] = None,
333+
/,
334+
*,
335+
return_factory: bool = False,
336+
):
337+
def decorator(wp):
338+
if not return_factory and isclass(wp):
339+
wp.__init__ = decorator(wp.__init__)
340+
return wp
341+
342+
lazy_binder = Lazy[Binder](lambda: self.__new_binder(wp))
343+
344+
@wraps(wp)
345+
def wrapper(*args, **kwargs):
346+
arguments = (~lazy_binder).bind(*args, **kwargs)
347+
return wp(*arguments.args, **arguments.kwargs)
348+
349+
return wrapper
350+
351+
return decorator(wrapped) if wrapped else decorator
352+
321353
def get_instance(self, cls: type[_T]) -> _T | None:
322354
try:
323355
injectable = self[cls]
@@ -420,6 +452,12 @@ def __move_module(self, module: Module, priority: ModulePriorities):
420452
f"`{module}` can't be found in the modules used by `{self}`."
421453
) from exc
422454

455+
def __new_binder(self, target: Callable[..., Any]) -> Binder:
456+
signature = inspect.signature(target, eval_str=True)
457+
binder = Binder(signature).update(self)
458+
self.add_listener(binder)
459+
return binder
460+
423461

424462
"""
425463
Binder
@@ -502,85 +540,3 @@ def on_event(self, event: Event, /):
502540
def _(self, event: ModuleEvent, /) -> ContextManager:
503541
yield
504542
self.update(event.on_module)
505-
506-
507-
"""
508-
Decorators
509-
"""
510-
511-
512-
@final
513-
@dataclass(repr=False, frozen=True, slots=True)
514-
class InjectDecorator:
515-
__module: Module
516-
517-
def __call__(self, wrapped: Callable[..., Any] = None, /):
518-
def decorator(wp):
519-
if isclass(wp):
520-
return self.__class_decorator(wp)
521-
522-
return self.__decorator(wp)
523-
524-
return decorator(wrapped) if wrapped else decorator
525-
526-
def __decorator(self, function: Callable[..., Any], /) -> Callable[..., Any]:
527-
lazy_binder = Lazy[Binder](lambda: self.__new_binder(function))
528-
529-
@wraps(function)
530-
def wrapper(*args, **kwargs):
531-
arguments = (~lazy_binder).bind(*args, **kwargs)
532-
return function(*arguments.args, **arguments.kwargs)
533-
534-
return wrapper
535-
536-
def __class_decorator(self, cls: type, /) -> type:
537-
cls.__init__ = self.__decorator(cls.__init__)
538-
return cls
539-
540-
def __new_binder(self, function: Callable[..., Any]) -> Binder:
541-
signature = inspect.signature(function, eval_str=True)
542-
binder = Binder(signature).update(self.__module)
543-
self.__module.add_listener(binder)
544-
return binder
545-
546-
547-
@final
548-
@dataclass(repr=False, frozen=True, slots=True)
549-
class InjectableDecorator:
550-
__module: Module
551-
__injectable_type: type[BaseInjectable]
552-
553-
def __repr__(self) -> str:
554-
return f"<{self.__injectable_type.__qualname__} decorator>"
555-
556-
def __call__(
557-
self,
558-
wrapped: Callable[..., Any] = None,
559-
/,
560-
*,
561-
on: type | Types = None,
562-
):
563-
def decorator(wp):
564-
@self.__module.inject
565-
@wraps(wp, updated=())
566-
def factory(*args, **kwargs):
567-
return wp(*args, **kwargs)
568-
569-
injectable = self.__injectable_type(factory)
570-
classes = self.__get_classes(wp, on)
571-
self.__module.update(classes, injectable)
572-
return wp
573-
574-
return decorator(wrapped) if wrapped else decorator
575-
576-
@classmethod
577-
def __get_classes(cls, *objects: Any) -> Iterator[type | UnionType]:
578-
for obj in objects:
579-
if isinstance(obj, Iterable) and not isinstance(obj, type | str):
580-
yield from cls.__get_classes(*obj)
581-
582-
elif isfunction(obj):
583-
yield get_annotations(obj, eval_str=True).get("return")
584-
585-
else:
586-
yield obj

tests/core/test_module.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_instance(self) -> class_:
2525
__getitem__
2626
"""
2727

28-
def test_getitem_with_success_injectable(self, module):
28+
def test_getitem_with_success_return_injectable(self, module):
2929
injectable_w = self.get_test_injectable(SomeClass())
3030
module[SomeClass] = injectable_w
3131
assert module[SomeClass] is injectable_w

0 commit comments

Comments
 (0)