lagom.decorators.context_dependency_definition()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 39
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0187

Importance

Changes 0
Metric Value
cc 5
eloc 13
nop 1
dl 0
loc 39
rs 9.2833
c 0
b 0
f 0
ccs 10
cts 11
cp 0.9091
crap 5.0187
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