|
1
|
|
|
""" |
|
2
|
|
|
PRIVATE MODULE: do not import (from) it directly. |
|
3
|
|
|
|
|
4
|
|
|
This module contains the ``inject`` function and its required private |
|
5
|
|
|
functions. |
|
6
|
|
|
""" |
|
7
|
|
|
import functools |
|
8
|
|
|
import inspect |
|
9
|
|
|
from collections import ChainMap |
|
10
|
|
|
from functools import partial, lru_cache |
|
11
|
|
|
from pathlib import Path |
|
12
|
|
|
from typing import List, Dict, Any, Type, Tuple |
|
13
|
|
|
from jacked import _container |
|
14
|
|
|
from jacked._container import DEFAULT_CONTAINER |
|
15
|
|
|
from jacked._discover import discover |
|
16
|
|
|
from jacked._exceptions import InjectionError, InvalidUsageError |
|
17
|
|
|
from jacked._injectable import Injectable |
|
18
|
|
|
from jacked._typing import T |
|
19
|
|
|
from jacked.matchers._base_matcher import BaseMatcher |
|
20
|
|
|
|
|
21
|
|
|
|
|
22
|
|
|
def inject_here( |
|
23
|
|
|
hint: Type[T], |
|
24
|
|
|
*, |
|
25
|
|
|
container: _container.Container = _container.DEFAULT_CONTAINER |
|
26
|
|
|
) -> T: |
|
27
|
|
|
""" |
|
28
|
|
|
Usage example: |
|
29
|
|
|
|
|
30
|
|
|
some_inst = inject_here(SomeClass) |
|
31
|
|
|
|
|
32
|
|
|
:param hint: the type that hints what is to be returned. |
|
33
|
|
|
:param container: the Container from which the injectable is to be |
|
34
|
|
|
returned. |
|
35
|
|
|
:return: an injectable that corresponds to ``hint``. |
|
36
|
|
|
""" |
|
37
|
|
|
candidates = _get_candidates(hint, container) |
|
38
|
|
|
if not candidates: |
|
39
|
|
|
raise InjectionError('No suitable candidates for "{}".' |
|
40
|
|
|
.format(hint), hint) |
|
41
|
|
|
return _choose_candidate(candidates) |
|
42
|
|
|
|
|
43
|
|
|
|
|
44
|
|
|
def inject( |
|
45
|
|
|
decorated: callable = None, |
|
46
|
|
|
*, |
|
47
|
|
|
container: _container.Container = _container.DEFAULT_CONTAINER |
|
48
|
|
|
) -> callable: |
|
49
|
|
|
""" |
|
50
|
|
|
Decorator that will inject all parameters that were not already explicitly |
|
51
|
|
|
provided. |
|
52
|
|
|
|
|
53
|
|
|
Usage example: |
|
54
|
|
|
|
|
55
|
|
|
@inject |
|
56
|
|
|
def func(x: SomeClass): |
|
57
|
|
|
x.some_func() # x is now an instance of SomeClass. |
|
58
|
|
|
|
|
59
|
|
|
:param decorated: the callable that is decorated. |
|
60
|
|
|
:param container: the storage that is used that contains all |
|
61
|
|
|
``Injectables``. |
|
62
|
|
|
:return: a decorator. |
|
63
|
|
|
""" |
|
64
|
|
|
if decorated: |
|
65
|
|
|
_check_decorated(decorated) |
|
66
|
|
|
return functools.update_wrapper( |
|
67
|
|
|
lambda *args, **kwargs: _wrapper(decorated, container, |
|
68
|
|
|
*args, **kwargs), decorated) |
|
69
|
|
|
return partial(_decorator, container=container) |
|
70
|
|
|
|
|
71
|
|
|
|
|
72
|
|
|
def get_candidates( |
|
73
|
|
|
hint: T, |
|
74
|
|
|
*, |
|
75
|
|
|
container: _container.Container = DEFAULT_CONTAINER) -> List[T]: |
|
76
|
|
|
""" |
|
77
|
|
|
Return all candidates for the given type ``T`` and return them in a list. |
|
78
|
|
|
:param hint: the type for which candidates are to be returned. |
|
79
|
|
|
:param container: the container from which the injectables are fetched. |
|
80
|
|
|
:return: a list of candidates of type ``T``. |
|
81
|
|
|
""" |
|
82
|
|
|
return [c for c, _ in _get_candidates(hint, container)] |
|
83
|
|
|
|
|
84
|
|
|
|
|
85
|
|
|
def _decorator( |
|
86
|
|
|
decorated: callable, |
|
87
|
|
|
container: _container.Container) -> callable: |
|
88
|
|
|
# This function acts as the "actual decorator" if any arguments were passed |
|
89
|
|
|
# to `inject`. |
|
90
|
|
|
_check_decorated(decorated) |
|
91
|
|
|
return functools.update_wrapper( |
|
92
|
|
|
lambda *args, **kwargs: _wrapper(decorated, container, *args, |
|
93
|
|
|
**kwargs), decorated) |
|
94
|
|
|
|
|
95
|
|
|
|
|
96
|
|
|
def _check_decorated(decorated: callable): |
|
97
|
|
|
# This function validates the decorated object and raises upon an invalid |
|
98
|
|
|
# decoration. |
|
99
|
|
|
if isinstance(decorated, type): |
|
100
|
|
|
raise InvalidUsageError('The inject decorator can be used on ' |
|
101
|
|
|
'callables only.') |
|
102
|
|
|
params = inspect.signature(decorated).parameters |
|
103
|
|
|
for param_name in params: |
|
104
|
|
|
hint = params[param_name].annotation |
|
105
|
|
|
if hint != inspect.Parameter.empty: |
|
106
|
|
|
_check_hint(hint) |
|
107
|
|
|
|
|
108
|
|
|
|
|
109
|
|
|
def _check_hint(hint: Any): |
|
110
|
|
|
pass # TODO implement this. |
|
111
|
|
|
|
|
112
|
|
|
|
|
113
|
|
|
def _wrapper( |
|
114
|
|
|
decorated: callable, |
|
115
|
|
|
container: _container.Container, |
|
116
|
|
|
*args, |
|
117
|
|
|
**kwargs_): |
|
118
|
|
|
# This function is wrapped around the decorated object. It will collect |
|
119
|
|
|
# arguments and inject them to `decorated` by providing these arguments. |
|
120
|
|
|
signature = inspect.signature(decorated) |
|
121
|
|
|
|
|
122
|
|
|
if args: |
|
123
|
|
|
# If there are any ordered parameters given, filter them out of the |
|
124
|
|
|
# signature to prevent the search for injection candidates: |
|
125
|
|
|
filtered_params = list(signature.parameters.values())[len(args):] |
|
126
|
|
|
signature = inspect.Signature(filtered_params) |
|
127
|
|
|
|
|
128
|
|
|
# Collect the arguments for injection: |
|
129
|
|
|
arguments = _collect_arguments(signature, container) |
|
130
|
|
|
kwargs_ = ChainMap(kwargs_, arguments) # Note: kwargs_ takes precedence. |
|
131
|
|
|
|
|
132
|
|
|
# Now all arguments are collected, "inject" them into `decorated`: |
|
133
|
|
|
return decorated(*args, **kwargs_) |
|
134
|
|
|
|
|
135
|
|
|
|
|
136
|
|
|
def _collect_arguments( |
|
137
|
|
|
signature: inspect.Signature, |
|
138
|
|
|
container: _container.Container) -> Dict[str, object]: |
|
139
|
|
|
# This function tries to collect arguments for the given signature and |
|
140
|
|
|
# returns a dictionary that corresponds to that signature. |
|
141
|
|
|
result = {} |
|
142
|
|
|
for param_name in signature.parameters: |
|
143
|
|
|
if param_name in ('self', 'cls'): |
|
144
|
|
|
continue |
|
145
|
|
|
param = signature.parameters[param_name] |
|
146
|
|
|
hint = param.annotation |
|
147
|
|
|
# Get all candidates that could be injected according to `signature`: |
|
148
|
|
|
candidates = _get_candidates(hint, container) |
|
149
|
|
|
if not candidates: |
|
150
|
|
|
result[param_name] = param.default |
|
151
|
|
|
if param.default is inspect.Parameter.empty: |
|
152
|
|
|
raise InjectionError('No suitable candidates for "{}".' |
|
153
|
|
|
.format(param_name), param) |
|
154
|
|
|
else: |
|
155
|
|
|
# If there are multiple candidates, select one: |
|
156
|
|
|
result[param_name] = _choose_candidate(candidates) |
|
157
|
|
|
return result |
|
158
|
|
|
|
|
159
|
|
|
|
|
160
|
|
|
def _get_candidates( |
|
161
|
|
|
hint: T, |
|
162
|
|
|
container: _container.Container) -> List[Tuple[T, Injectable]]: |
|
163
|
|
|
# Search in the known injectables in `container` for all matching |
|
164
|
|
|
# candidates. The candidates are returned sorted by their priority. |
|
165
|
|
|
candidates = ((_match(hint, injectable, container), injectable) |
|
|
|
|
|
|
166
|
|
|
for injectable in container.injectables) |
|
167
|
|
|
result = [(c, i) for c, i in candidates if c] |
|
168
|
|
|
result.sort(key=lambda c: c[1].priority, reverse=True) |
|
169
|
|
|
return result |
|
170
|
|
|
|
|
171
|
|
|
|
|
172
|
|
|
def _choose_candidate(candidates: List[Tuple[T, Injectable]]) -> T: |
|
173
|
|
|
# From a list of candidates, pick and return one: |
|
174
|
|
|
return candidates[0][0] # The first should have the highest priority. |
|
175
|
|
|
|
|
176
|
|
|
|
|
177
|
|
|
def _match( |
|
178
|
|
|
hint: type, |
|
179
|
|
|
injectable: Injectable, |
|
180
|
|
|
container: _container.Container) -> object: |
|
181
|
|
|
# Check if the given `parameter` matches with the given `injectable`. If |
|
182
|
|
|
# there appears to be a match, return what is to be injected (e.g. an |
|
183
|
|
|
# instance of a class, a class itself, ...). If no match, return `None`. |
|
184
|
|
|
for matcher in _get_matchers(): |
|
185
|
|
|
if matcher.can_match(hint): |
|
186
|
|
|
# Match or no match, return anyway: |
|
187
|
|
|
return matcher.match(hint, injectable, container) |
|
188
|
|
|
|
|
189
|
|
|
|
|
190
|
|
|
@lru_cache() |
|
191
|
|
|
def _get_matchers() -> List[BaseMatcher]: |
|
192
|
|
|
path_to_matchers = str(Path(__file__).parent.joinpath(Path('matchers'))) |
|
193
|
|
|
modules = discover(path_to_matchers) |
|
194
|
|
|
|
|
195
|
|
|
public_elements = [getattr(mod, elem) for mod in modules |
|
196
|
|
|
for elem in dir(mod) if not elem.startswith('_')] |
|
197
|
|
|
matchers = [cls() for cls in public_elements if isinstance(cls, type) |
|
198
|
|
|
and cls is not BaseMatcher and issubclass(cls, BaseMatcher)] |
|
199
|
|
|
|
|
200
|
|
|
matchers.sort(key=lambda m: m.priority(), reverse=True) |
|
201
|
|
|
|
|
202
|
|
|
return matchers |
|
203
|
|
|
|