Passed
Pull Request — master (#226)
by Steve
02:52
created

lagom.wrapping.RegularFunc.__call__()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nop 3
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
"""
2
Code in this module is used to wrap and decorate functions that have been
3
bound to a container
4
"""
5 1
import functools
6 1
import inspect
7 1
from typing import Protocol, Any
8
9 1
from .exceptions import UnableToInvokeBoundFunction
10 1
from .injection_context import TemporaryInjectionContext
11 1
from .interfaces import ReadableContainer, ContainerBoundFunction
12 1
from .util.reflection import FunctionSpec
13
14
15 1
class _Callable(Protocol):
16
    """
17
    This is a bit of a hack so that a callable can be provided to a class without mypy thinking its a class method
18
    """
19
20 1
    def __call__(self, *args, **kwargs) -> Any:
21
        pass
22
23
24 1
class RegularFunc(ContainerBoundFunction):
25
    """
26
    Represents a function that has been bound to a container
27
    """
28
29 1
    _argument_updater: _Callable
30 1
    _base_injection_context: TemporaryInjectionContext
31 1
    _inner_func: _Callable
32
33 1
    def __init__(self, inner_func, base_injection_context, argument_updater):
34 1
        self._inner_func = inner_func
35 1
        self._base_injection_context = base_injection_context
36 1
        self._argument_updater = argument_updater
37
38 1
    def __call__(self, *args, **kwargs):
39 1
        argument_updater = self._argument_updater
40 1
        inner_func = self._inner_func
41 1
        bound_args, bound_kwargs = argument_updater(
42
            self._base_injection_context, args, kwargs
43
        )
44 1
        return inner_func(*bound_args, **bound_kwargs)
45
46 1
    def rebind(self, container: ReadableContainer):
47 1
        return RegularFunc(
48
            self._inner_func,
49
            self._base_injection_context.rebind(container),
50
            self._argument_updater,
51
        )
52
53
54 1
class AsyncFunc(ContainerBoundFunction):
55
    """
56
    Represents an async function that has been bound to a container
57
    """
58
59 1
    _argument_updater: _Callable
60 1
    _base_injection_context: TemporaryInjectionContext
61 1
    _inner_func: _Callable
62
63 1
    def __init__(self, inner_func, base_injection_context, argument_updater):
64 1
        self._inner_func = inner_func
65 1
        self._base_injection_context = base_injection_context
66 1
        self._argument_updater = argument_updater
67
68 1
    def __call__(self, *args, **kwargs):
69
        return self.__async_call__(*args, **kwargs)
70
71 1
    async def __async_call__(self, *args, **kwargs):
72 1
        argument_updater = self._argument_updater
73 1
        inner_func = self._inner_func
74 1
        bound_args, bound_kwargs = argument_updater(
75
            self._base_injection_context, args, kwargs
76
        )
77 1
        return await inner_func(*bound_args, **bound_kwargs)
78
79 1
    def as_coroutine(self):
80
        """
81
        returns a coroutine that wraps this class. Some class methods
82
        also get added to the coroutine. This is so it acts like a class with
83
        an __asynccall__ magic method.
84
        """
85
86 1
        async def _coroutine_func(*args, **kwargs):
87 1
            return await self.__async_call__(*args, **kwargs)
88
89 1
        _coroutine_func.rebind = self.rebind  # type: ignore
90
91 1
        return _coroutine_func
92
93 1
    def rebind(self, container: ReadableContainer):
94 1
        return AsyncFunc(
95
            self._inner_func,
96
            self._base_injection_context.rebind(container),
97
            self._argument_updater,
98
        ).as_coroutine()
99
100
101 1
def apply_argument_updater(
102
    func,
103
    base_injection_context,
104
    argument_updater,
105
    spec: FunctionSpec,
106
    catch_errors=False,
107
) -> ContainerBoundFunction:
108
    """
109
    Takes a function and binds it to a container with an update function
110
    """
111 1
    inner_func = func if not catch_errors else _wrap_func_in_error_handling(func, spec)
112 1
    if inspect.iscoroutinefunction(func):
113
114 1
        _bound_func = AsyncFunc(
115
            inner_func, base_injection_context, argument_updater
116
        ).as_coroutine()
117
118
    else:
119
120 1
        _bound_func = RegularFunc(inner_func, base_injection_context, argument_updater)
121
122 1
    return functools.wraps(func)(_bound_func)
123
124
125 1
def _wrap_func_in_error_handling(func, spec: FunctionSpec):
126
    """
127
    Takes a func and its spec and returns a function that's the same
128
    but with more useful TypeError messages
129
    :param func:
130
    :param spec:
131
    :return:
132
    """
133
134 1
    @functools.wraps(func)
135 1
    def _error_handling_func(*args, **kwargs):
136 1
        try:
137 1
            return func(*args, **kwargs)
138 1
        except TypeError as error:
139
            # if it wasn't in kwargs the container couldn't build it
140 1
            unresolvable_deps = [
141
                dep_type
142
                for (name, dep_type) in spec.annotations.items()
143
                if name not in kwargs.keys()
144
            ]
145 1
            raise UnableToInvokeBoundFunction(str(error), unresolvable_deps)
146
147
    return _error_handling_func
148