lagom.decorators   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 163
Duplicated Lines 0 %

Test Coverage

Coverage 98.08%

Importance

Changes 0
Metric Value
wmc 16
eloc 73
dl 0
loc 163
ccs 51
cts 52
cp 0.9808
rs 10
c 0
b 0
f 0

6 Functions

Rating   Name   Duplication   Size   Complexity  
A bind_to_container() 0 9 2
A context_dependency_definition() 0 39 5
A _extract_definition_func_and_type() 0 21 4
A dependency_definition() 0 25 2
A magic_bind_to_container() 0 21 2
A _generator_type() 0 2 1
1
"""
2
This module provides decorators for hooking an
3
application into the container.s
4
"""
5
6 1
import inspect
7 1
from contextlib import contextmanager, asynccontextmanager
8 1
from functools import wraps
9 1
from types import FunctionType
10 1
from typing import (
11
    List,
12
    Type,
13
    Callable,
14
    Tuple,
15
    TypeVar,
16
    ContextManager,
17
    AsyncContextManager,
18
    Optional,
19
)
20
21 1
from .container import Container
22 1
from .definitions import Singleton, construction, yielding_construction
23 1
from .exceptions import (
24
    MissingReturnType,
25
    ClassesCannotBeDecorated,
26
    InvalidDependencyDefinition,
27
)
28 1
from .interfaces import SpecialDepDefinition
29 1
from .util.reflection import reflect
30
31 1
T = TypeVar("T")
32 1
R = TypeVar("R")
33
34
35 1
def bind_to_container(
36
    container: Container, shared: Optional[List[Type]] = None
37
) -> Callable[[Callable[..., R]], Callable[..., R]]:
38 1
    def _decorator(func):
39 1
        if not isinstance(func, FunctionType):
40 1
            raise ClassesCannotBeDecorated()
41 1
        return wraps(func)(container.partial(func, shared=shared))
42
43 1
    return _decorator
44
45
46 1
def magic_bind_to_container(
47
    container: Container, shared: Optional[List[Type]] = None
48
) -> Callable[[Callable[..., R]], Callable[..., R]]:
49
    """Decorates the function so that it's uses the container to construct things
50
51
    >>> from tests.examples import SomeClass
52
    >>> c = Container()
53
    >>> @magic_bind_to_container(c)
54
    ... def say_hello_from(sayer: SomeClass):
55
    ...    return f"hello from {sayer}"
56
    >>> say_hello_from()
57
    'hello from <tests.examples.SomeClass object at ...>'
58
59
    """
60
61 1
    def _decorator(func):
62 1
        if not isinstance(func, FunctionType):
63 1
            raise ClassesCannotBeDecorated()
64 1
        return wraps(func)(container.magic_partial(func, shared=shared))
65
66 1
    return _decorator
67
68
69 1
def dependency_definition(container: Container, singleton: bool = False):
70
    """Registers the provided function with the container
71
    The return type of the decorated function will be reflected and whenever
72
    the container is asked for this type the function will be called
73
74
    >>> from tests.examples import SomeClass, SomeExtendedClass
75
    >>> c = Container()
76
    >>> @dependency_definition(c)
77
    ... def build_some_class_but_extended() -> SomeClass:
78
    ...    return SomeExtendedClass()
79
    >>> c.resolve(SomeClass)
80
    <tests.examples.SomeExtendedClass object at ...>
81
82
    """
83
84 1
    def _decorator(func):
85 1
        definition_func, return_type = _extract_definition_func_and_type(func)  # type: ignore
86
87 1
        if singleton:
88 1
            container.define(return_type, Singleton(definition_func))
89
        else:
90 1
            container.define(return_type, definition_func)
91 1
        return func
92
93 1
    return _decorator
94
95
96 1
def context_dependency_definition(container: Container):
97
    """
98
    Turns the decorated function into a definition for a context manager
99
    in the given container.
100
101
    >>> from tests.examples import SomeClass
102
    >>> from typing import Iterator
103
    >>>
104
    >>> c = Container()
105
    >>>
106
    >>> @context_dependency_definition(c)
107
    ... def my_constructor() -> Iterator[SomeClass]:
108
    ...     try:
109
    ...         yield SomeClass()
110
    ...     finally:
111
    ...         pass # Any tidy up or resource closing could happen here
112
    >>> with c[ContextManager[SomeClass]] as something:
113
    ...     something
114
    <tests.examples.SomeClass ...>
115
116
    with container[ContextManager[MyComplexDep]] as dep:  # type: ignore
117
        assert dep.some_number == 3
118
    """
119
120 1
    def _decorator(func):
121 1
        if not inspect.isgeneratorfunction(func) and not inspect.isasyncgenfunction(
122
            func
123
        ):
124
            raise InvalidDependencyDefinition(
125
                "context_dependency_definition must be given a generator"
126
            )
127 1
        dep_type = _generator_type(reflect(func).return_type)
128 1
        if inspect.isgeneratorfunction(func):
129 1
            container.define(ContextManager[dep_type], contextmanager(func))  # type: ignore
130 1
        if inspect.isasyncgenfunction(func):
131 1
            container.define(AsyncContextManager[dep_type], asynccontextmanager(func))  # type: ignore
132 1
        return func
133
134 1
    return _decorator
135
136
137 1
def _extract_definition_func_and_type(
138
    func,
139
) -> Tuple[SpecialDepDefinition, Type[T]]:
140
    """
141
    Takes a function or a generator and returns a function and the return type.
142
    :param func:
143
    :return:
144
    """
145
146 1
    return_type = reflect(func).return_type
147 1
    if not return_type:
148 1
        raise MissingReturnType(
149
            f"Function {func.__name__} used as a definition must have a return type"
150
        )
151
152 1
    if not inspect.isgeneratorfunction(func) and not inspect.isasyncgenfunction(func):
153 1
        return construction(func), return_type
154
155 1
    return (
156
        yielding_construction(func),
157
        _generator_type(return_type),
158
    )
159
160
161 1
def _generator_type(return_type):
162
    return return_type.__args__[0]  # todo: something less hacky
163