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
|
|
|
|