Test Failed
Pull Request — master (#226)
by Steve
02:44
created

lagom.wrapping.RegularFunc.rebind()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nop 2
dl 0
loc 5
ccs 2
cts 2
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
from typing import Protocol, Any
8 1
9 1
from .exceptions import UnableToInvokeBoundFunction
10
from .injection_context import TemporaryInjectionContext
11
from .interfaces import ReadableContainer, ContainerBoundFunction
12 1
from .util.reflection import FunctionSpec
13
14
15 1
class _Callable(Protocol):
16 1
    """
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 1
    """
19 1
20 1
    def __call__(self, *args, **kwargs) -> Any:
21 1
        pass
22
23
24
class RegularFunc(ContainerBoundFunction):
25 1
    """
26 1
    Represents a function that has been bound to a container
27 1
    """
28 1
29
    _argument_updater: _Callable
30 1
    _base_injection_context: TemporaryInjectionContext
31
    _inner_func: _Callable
32
33 1
    def __init__(self, inner_func, base_injection_context, argument_updater):
34
        self._inner_func = inner_func
35
        self._base_injection_context = base_injection_context
36
        self._argument_updater = argument_updater
37
38
    def __call__(self, *args, **kwargs):
39
        argument_updater = self._argument_updater
40
        inner_func = self._inner_func
41
        bound_args, bound_kwargs = argument_updater(
42 1
            self._base_injection_context, args, kwargs
43 1
        )
44 1
        return inner_func(*bound_args, **bound_kwargs)
45 1
46 1
    def rebind(self, container: ReadableContainer):
47
        return RegularFunc(
48 1
            self._inner_func,
49
            self._base_injection_context.rebind(container),
50
            self._argument_updater,
51
        )
52
53 1
54
class AsyncFunc(ContainerBoundFunction):
55 1
    """
56
    Represents an async function that has been bound to a container
57
    """
58
59
    _argument_updater: _Callable
60
    _base_injection_context: TemporaryInjectionContext
61
    _inner_func: _Callable
62
63
    def __init__(self, inner_func, base_injection_context, argument_updater):
64
        self._inner_func = inner_func
65
        self._base_injection_context = base_injection_context
66
        self._argument_updater = argument_updater
67
68
    def __call__(self, *args, **kwargs):
69
        return self.__async_call__(*args, **kwargs)
70
71
    async def __async_call__(self, *args, **kwargs):
72
        argument_updater = self._argument_updater
73
        inner_func = self._inner_func
74
        bound_args, bound_kwargs = argument_updater(
75
            self._base_injection_context, args, kwargs
76
        )
77
        return await inner_func(*bound_args, **bound_kwargs)
78
79
    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
        async def _coroutine_func(*args, **kwargs):
87
            return await self.__async_call__(*args, **kwargs)
88
89
        _coroutine_func.rebind = self.rebind  # type: ignore
90
91
        return _coroutine_func
92
93
    def rebind(self, container: ReadableContainer):
94
        return AsyncFunc(
95
            self._inner_func,
96
            self._base_injection_context.rebind(container),
97
            self._argument_updater,
98
        ).as_coroutine()
99
100
101
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
    inner_func = func if not catch_errors else _wrap_func_in_error_handling(func, spec)
112
    if inspect.iscoroutinefunction(func):
113
114
        _bound_func = AsyncFunc(
115
            inner_func, base_injection_context, argument_updater
116
        ).as_coroutine()
117
118
    else:
119
120
        _bound_func = RegularFunc(inner_func, base_injection_context, argument_updater)
121
122
    return functools.wraps(func)(_bound_func)
123
124
125
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
    @functools.wraps(func)
135
    def _error_handling_func(*args, **kwargs):
136
        try:
137
            return func(*args, **kwargs)
138
        except TypeError as error:
139
            # if it wasn't in kwargs the container couldn't build it
140
            unresolvable_deps = [
141
                dep_type
142
                for (name, dep_type) in spec.annotations.items()
143
                if name not in kwargs.keys()
144
            ]
145
            raise UnableToInvokeBoundFunction(str(error), unresolvable_deps)
146
147
    return _error_handling_func
148