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

lagom.container   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Test Coverage

Coverage 98.8%

Importance

Changes 0
Metric Value
eloc 269
dl 0
loc 498
ccs 164
cts 166
cp 0.988
rs 7.92
c 0
b 0
f 0
wmc 51

21 Methods

Rating   Name   Duplication   Size   Complexity  
A ExplicitContainer.clone() 0 5 1
A Container.clone() 0 5 1
A Container.partial() 0 46 3
A Container._reflection_build_with_err_handling() 0 13 5
A Container.magic_partial() 0 47 3
A Container.resolve() 0 41 4
B Container.define() 0 33 6
A Container.__init__() 0 30 5
A Container.temporary_singletons() 0 23 2
A Container._infer_dependencies() 0 20 1
A EmptyDefinitionSet.get_definition() 0 7 1
A ExplicitContainer.resolve() 0 9 3
A Container._reflection_build() 0 11 2
A Container.__getitem__() 0 2 1
A ExplicitContainer.define() 0 13 4
A Container.reflection_cache_overview() 0 3 1
A Container.defined_types() 0 8 1
A Container._get_spec_without_self() 0 5 2
A Container.__setitem__() 0 2 1
A EmptyDefinitionSet.defined_types() 0 3 1
A Container.get_definition() 0 12 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A _update_nothing() 0 2 1

How to fix   Complexity   

Complexity

Complex classes like lagom.container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 1
import functools
2 1
import io
3 1
import logging
4 1
import typing
5
6 1
from .compilaton import mypyc_attr
7
8 1
from types import FunctionType, MethodType
9 1
from typing import (
10
    Dict,
11
    Type,
12
    Any,
13
    TypeVar,
14
    Callable,
15
    Set,
16
    List,
17
    Optional,
18
    cast,
19
    Union,
20
)
21
22 1
from .definitions import (
23
    normalise,
24
    Singleton,
25
    Alias,
26
    ConstructionWithoutContainer,
27
    UnresolvableTypeDefinition,
28
)
29 1
from .exceptions import (
30
    UnresolvableType,
31
    DuplicateDefinition,
32
    InvalidDependencyDefinition,
33
    RecursiveDefinitionError,
34
    DependencyNotDefined,
35
    TypeOnlyAvailableAsAwaitable,
36
)
37 1
from .injection_context import TemporaryInjectionContext
38
from .interfaces import (
39
    SpecialDepDefinition,
40
    WriteableContainer,
41
    TypeResolver,
42
    DefinitionsSource,
43
    ExtendableContainer,
44
    ContainerDebugInfo,
45
    CallTimeContainerUpdate,
46 1
    ContainerBoundFunction,
47 1
)
48 1
from .markers import injectable
49 1
from .updaters import update_container_singletons
50
from .util.logging import NullLogger
51
from .util.reflection import (
52
    FunctionSpec,
53
    CachingReflector,
54
    remove_optional_type,
55 1
    remove_awaitable_type,
56
)
57 1
from .wrapping import apply_argument_updater
58
59
UNRESOLVABLE_TYPES = [
60
    str,
61
    int,
62
    float,
63
    bool,
64
    bytes,
65
    bytearray,
66
    io.BytesIO,
67
    io.BufferedIOBase,
68
    io.BufferedRandom,
69
    io.BufferedReader,
70
    io.BufferedRWPair,
71
    io.BufferedWriter,
72
    io.FileIO,
73
    io.IOBase,
74
    io.RawIOBase,
75
    io.TextIOBase,
76
    typing.IO,
77
    typing.TextIO,
78
    typing.BinaryIO,
79 1
]
80
81 1
X = TypeVar("X")
82
83
Unset: Any = object()
84 1
85 1
86
@mypyc_attr(allow_interpreted_subclasses=True)
87
class Container(
88
    WriteableContainer, ExtendableContainer, DefinitionsSource, ContainerDebugInfo
89
):
90
    """Dependency injection container
91
92
    Lagom is a dependency injection container designed to give you "just enough"
93
    help with building your dependencies. The intention is that almost
94
    all of your code doesn't know about or rely on lagom. Lagom will
95
    only be involved at the top level to pull everything together.
96
97
    >>> from tests.examples import SomeClass
98
    >>> c = Container()
99
    >>> c[SomeClass]
100
    <tests.examples.SomeClass object at ...>
101
102
    Objects are constructed as they are needed
103
104
    >>> from tests.examples import SomeClass
105
    >>> c = Container()
106
    >>> first = c[SomeClass]
107
    >>> second = c[SomeClass]
108
    >>> first != second
109
    True
110
111
    And construction logic can be defined
112
    >>> from tests.examples import SomeClass, SomeExtendedClass
113
    >>> c = Container()
114
    >>> c[SomeClass] = SomeExtendedClass
115
    >>> c[SomeClass]
116
    <tests.examples.SomeExtendedClass object at ...>
117 1
    """
118 1
119 1
    _registered_types: Dict[Type, SpecialDepDefinition]
120 1
    _parent_definitions: DefinitionsSource
121
    _reflector: CachingReflector
122 1
    _undefined_logger: logging.Logger
123
124
    def __init__(
125
        self,
126
        container: Optional["Container"] = None,
127
        log_undefined_deps: Union[bool, logging.Logger] = False,
128
    ):
129
        """
130
        :param container: Optional container if provided the existing definitions will be copied
131
        :param log_undefined_deps indicates if a log message should be emmited when an undefined dep is loaded
132
        """
133
134
        # ContainerDebugInfo is always registered
135 1
        # This means consumers can consume an overview of the container
136
        # without hacking anything custom together.
137
        self._registered_types = {
138
            ContainerDebugInfo: ConstructionWithoutContainer(lambda: self)
139 1
        }
140 1
141 1
        if container:
142
            self._parent_definitions = container
143 1
            self._reflector = container._reflector
144 1
        else:
145
            self._parent_definitions = EmptyDefinitionSet()
146 1
            self._reflector = CachingReflector()
147 1
148 1
        if not log_undefined_deps:
149 1
            self._undefined_logger = NullLogger()
150
        elif log_undefined_deps is True:
151 1
            self._undefined_logger = logging.getLogger(__name__)
152
        else:
153 1
            self._undefined_logger = cast(logging.Logger, log_undefined_deps)
154
155
    def define(self, dep: Type[X], resolver: TypeResolver[X]) -> SpecialDepDefinition:
156
        """Register how to construct an object of type X
157
158
        >>> from tests.examples import SomeClass
159
        >>> c = Container()
160
        >>> c.define(SomeClass, lambda: SomeClass())
161
        <lagom.definitions.ConstructionWithoutContainer ...>
162
163
        :param dep: The type to be constructed
164
        :param resolver: A definition of how to construct it
165 1
        :return:
166
        """
167 1
        if dep in UNRESOLVABLE_TYPES:
168 1
            raise InvalidDependencyDefinition()
169 1
        if dep in self._registered_types:
170
            raise DuplicateDefinition()
171 1
        if dep is resolver:
172 1
            # This is a special case for things like container[Foo] = Foo
173 1
            return self.define(dep, Alias(dep, skip_definitions=True))
174 1
        definition = normalise(resolver)
175
        self._registered_types[dep] = definition
176
        self._registered_types[Optional[dep]] = definition  # type: ignore
177
178 1
        # For awaitables we add a convenience exception to be thrown if code hints on the type
179 1
        # without the awaitable.
180
        awaitable_type = remove_awaitable_type(dep)
181 1
        if awaitable_type:
182 1
            # Unless there's already a sync version defined.
183
            if awaitable_type not in self.defined_types:
184
                self._registered_types[awaitable_type] = UnresolvableTypeDefinition(
185 1
                    TypeOnlyAvailableAsAwaitable(awaitable_type)
186
                )
187 1
        return definition
188 1
189
    @property
190
    def defined_types(self) -> Set[Type]:
191
        """The types the container has explicit build instructions for
192
193 1
        :return:
194
        """
195
        return self._parent_definitions.defined_types.union(
196
            self._registered_types.keys()
197 1
        )
198 1
199 1
    @property
200
    def reflection_cache_overview(self) -> Dict[str, str]:
201 1
        return self._reflector.overview_of_cache
202
203
    def temporary_singletons(
204
        self, singletons: Optional[List[Type]] = None
205
    ) -> "TemporaryInjectionContext":
206
        """
207
        Returns a context that loads a new container with singletons that only exist
208
        for the context.
209
210
        >>> from tests.examples import SomeClass
211
        >>> base_container = Container()
212
        >>> def my_func():
213
        ...     with base_container.temporary_singletons([SomeClass]) as c:
214
        ...         assert c[SomeClass] is c[SomeClass]
215
        >>> my_func()
216
217
        :param singletons: items which should be considered singletons within the context
218 1
        :return:
219
        """
220
        updater = (
221
            functools.partial(update_container_singletons, singletons=singletons)
222
            if singletons
223 1
            else None
224
        )
225 1
        return TemporaryInjectionContext(self, updater)
226
227
    def resolve(
228
        self, dep_type: Type[X], suppress_error=False, skip_definitions=False
229
    ) -> X:
230
        """Constructs an object of type X
231
232
         If the object can't be constructed an exception will be raised unless
233
         supress errors is true
234
235
        >>> from tests.examples import SomeClass
236
        >>> c = Container()
237
        >>> c.resolve(SomeClass)
238
        <tests.examples.SomeClass object at ...>
239
240
        >>> from tests.examples import SomeClass
241
        >>> c = Container()
242
        >>> c.resolve(int)
243
        Traceback (most recent call last):
244
        ...
245
        lagom.exceptions.UnresolvableType: ...
246
247
        Optional wrappers are stripped out to be what is being asked for
248
        >>> from tests.examples import SomeClass
249
        >>> c = Container()
250
        >>> c.resolve(Optional[SomeClass])
251
        <tests.examples.SomeClass object at ...>
252
253
        :param dep_type: The type of object to construct
254
        :param suppress_error: if true returns None on failure
255
        :param skip_definitions:
256 1
        :return:
257 1
        """
258 1
        if not skip_definitions:
259 1
            definition = self.get_definition(dep_type)
260
            if definition:
261 1
                return definition.get_instance(self)
262 1
263 1
        optional_dep_type = remove_optional_type(dep_type)
264
        if optional_dep_type:
265 1
            return self.resolve(optional_dep_type, suppress_error=True)
266
267 1
        return self._reflection_build_with_err_handling(dep_type, suppress_error)
268
269
    def partial(
270
        self,
271
        func: Callable[..., X],
272
        shared: Optional[List[Type]] = None,
273
        container_updater: Optional[CallTimeContainerUpdate] = None,
274
    ) -> ContainerBoundFunction[X]:
275
        """Takes a callable and returns a callable bound to the container
276
        When invoking the new callable if any arguments have a default set
277
        to the special marker object "injectable" then they will be constructed by
278
        the container. For automatic injection without the marker use "magic_partial"
279
        >>> from tests.examples import SomeClass
280
        >>> c = Container()
281
        >>> def my_func(something: SomeClass = injectable):
282
        ...     return f"Successfully called with {something}"
283
        >>> bound_func = c.magic_partial(my_func)
284
        >>> bound_func()
285
        'Successfully called with <tests.examples.SomeClass object at ...>'
286
287
        :param func: the function to bind to the container
288
        :param shared: items which should be considered singletons on a per call level
289
        :param container_updater: An optional callable to update the container before resolution
290 1
        :return:
291 1
        """
292
        spec = self._get_spec_without_self(func)
293
        keys_to_bind = (
294 1
            key for (key, arg) in spec.defaults.items() if arg is injectable
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable key does not seem to be defined.
Loading history...
295
        )
296 1
        keys_and_types = [(key, spec.annotations[key]) for key in keys_to_bind]
297 1
298
        base_injection_context = self.temporary_singletons(shared)
299 1
        update_container = container_updater if container_updater else _update_nothing
300 1
301 1
        def _update_args(_injection_context, supplied_args, supplied_kwargs):
302 1
            keys_to_skip = set(supplied_kwargs.keys())
303 1
            keys_to_skip.update(spec.args[0 : len(supplied_args)])
304 1
            with _injection_context as invocation_container:
305
                update_container(invocation_container, supplied_args, supplied_kwargs)
306
                kwargs = {
307
                    key: invocation_container.resolve(dep_type)
308
                    for (key, dep_type) in keys_and_types
309 1
                    if key not in keys_to_skip
310 1
                }
311
            kwargs.update(supplied_kwargs)
312 1
            return supplied_args, kwargs
313
314 1
        return apply_argument_updater(func, base_injection_context, _update_args, spec)
315
316
    def magic_partial(
317
        self,
318
        func: Callable[..., X],
319
        shared: Optional[List[Type]] = None,
320
        keys_to_skip: Optional[List[str]] = None,
321
        skip_pos_up_to: int = 0,
322
        container_updater: Optional[CallTimeContainerUpdate] = None,
323
    ) -> ContainerBoundFunction[X]:
324
        """Takes a callable and returns a callable bound to the container
325
        When invoking the new callable if any arguments can be constructed by the container
326
        then they can be ommited.
327
        >>> from tests.examples import SomeClass
328
        >>> c = Container()
329
        >>> def my_func(something: SomeClass):
330
        ...   return f"Successfully called with {something}"
331
        >>> bound_func = c.magic_partial(my_func)
332
        >>> bound_func()
333
        'Successfully called with <tests.examples.SomeClass object at ...>'
334
335
        :param func: the function to bind to the container
336
        :param shared: items which should be considered singletons on a per call level
337
        :param keys_to_skip: named arguments which the container shouldnt build
338
        :param skip_pos_up_to: positional arguments which the container shouldnt build
339
        :param container_updater: An optional callable to update the container before resolution
340 1
        :return:
341
        """
342 1
        spec = self._get_spec_without_self(func)
343 1
344
        update_container = container_updater if container_updater else _update_nothing
345 1
        base_injection_context = self.temporary_singletons(shared)
346 1
347 1
        def _update_args(_injection_context, supplied_args, supplied_kwargs):
348 1
            final_keys_to_skip = (keys_to_skip or []) + list(supplied_kwargs.keys())
349 1
            final_skip_pos_up_to = max(skip_pos_up_to, len(supplied_args))
350 1
            with _injection_context as invocation_container:
351
                update_container(invocation_container, supplied_args, supplied_kwargs)
352
                kwargs = invocation_container._infer_dependencies(
353
                    spec,
354
                    suppress_error=True,
355
                    keys_to_skip=final_keys_to_skip,
356 1
                    skip_pos_up_to=final_skip_pos_up_to,
357 1
                )
358
            kwargs.update(supplied_kwargs)
359 1
            return supplied_args, kwargs
360
361 1
        return apply_argument_updater(
362
            func, base_injection_context, _update_args, spec, catch_errors=True
363
        )
364
365 1
    def clone(self) -> "Container":
366
        """returns a copy of the container
367 1
        :return:
368
        """
369
        return Container(self, log_undefined_deps=self._undefined_logger)
370
371
    def get_definition(self, dep_type: Type[X]) -> Optional[SpecialDepDefinition[X]]:
372
        """
373
        Will return the definition in this container. If none has been defined any
374
        definition in the parent container will be used.
375 1
376 1
        :param dep_type:
377 1
        :return:
378 1
        """
379
        definition = self._registered_types.get(dep_type, Unset)
380 1
        if definition is Unset:
381 1
            return self._parent_definitions.get_definition(dep_type)
382
        return definition
383 1
384 1
    def __getitem__(self, dep: Type[X]) -> X:
385
        return self.resolve(dep)
386 1
387
    def __setitem__(self, dep: Type[X], resolver: TypeResolver[X]):
388
        self.define(dep, resolver)
389 1
390 1
    def _reflection_build_with_err_handling(
391 1
        self, dep_type: Type[X], suppress_error: bool
392 1
    ) -> X:
393 1
        try:
394 1
            if dep_type in UNRESOLVABLE_TYPES:
395 1
                raise UnresolvableType(dep_type)
396 1
            return self._reflection_build(dep_type)
397 1
        except UnresolvableType as inner_error:
398
            if not suppress_error:
399
                raise UnresolvableType(dep_type) from inner_error
400 1
            return None  # type: ignore
401 1
        except RecursionError as recursion_error:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable RecursionError does not seem to be defined.
Loading history...
402
            raise RecursiveDefinitionError(dep_type) from recursion_error
403
404
    def _reflection_build(self, dep_type: Type[X]) -> X:
405 1
        self._undefined_logger.warning(
406 1
            f"Undefined dependency. Using reflection for {dep_type}",
407 1
            extra={"undefined_dependency": dep_type},
408 1
        )
409 1
        spec = self._reflector.get_function_spec(dep_type.__init__)
410 1
        sub_deps = self._infer_dependencies(spec, types_to_skip={dep_type})
411
        try:
412 1
            return dep_type(**sub_deps)  # type: ignore
413
        except TypeError as type_error:
414
            raise UnresolvableType(dep_type) from type_error
415
416
    def _infer_dependencies(
417
        self,
418
        spec: FunctionSpec,
419
        suppress_error=False,
420 1
        keys_to_skip: Optional[List[str]] = None,
421 1
        skip_pos_up_to=0,
422 1
        types_to_skip: Optional[Set[Type]] = None,
423 1
    ):
424 1
        dep_keys_to_skip: List[str] = []
425
        dep_keys_to_skip.extend(spec.args[0:skip_pos_up_to])
426
        dep_keys_to_skip.extend(keys_to_skip or [])
427
        types_to_skip = types_to_skip or set()
428
        sub_deps = {
429
            key: self.resolve(sub_dep_type, suppress_error=suppress_error)
430
            for (key, sub_dep_type) in spec.annotations.items()
431 1
            if sub_dep_type != Any
432
            and (key not in dep_keys_to_skip)
433 1
            and (sub_dep_type not in types_to_skip)
434 1
        }
435 1
        return {key: dep for (key, dep) in sub_deps.items() if dep is not None}
436 1
437 1
    def _get_spec_without_self(self, func: Callable[..., X]) -> FunctionSpec:
438
        if isinstance(func, (FunctionType, MethodType)):
439
            return self._reflector.get_function_spec(func)
440 1
        t = cast(Type[X], func)
441 1
        return self._reflector.get_function_spec(t.__init__).without_argument("self")
442 1
443
444
@mypyc_attr(allow_interpreted_subclasses=True)
445 1
class ExplicitContainer(Container):
446 1
    def resolve(
447 1
        self, dep_type: Type[X], suppress_error=False, skip_definitions=False
448 1
    ) -> X:
449 1
        definition = self.get_definition(dep_type)
450 1
        if not definition:
451
            if suppress_error:
452 1
                return None  # type: ignore
453 1
            raise DependencyNotDefined(dep_type)
454 1
        return definition.get_instance(self)
455 1
456
    def define(self, dep, resolver):
457
        definition = super().define(dep, resolver)
458 1
        if isinstance(definition, Alias):
459
            raise InvalidDependencyDefinition(
460
                "Aliases are not valid in an explicit container"
461 1
            )
462
        if isinstance(definition, Singleton) and isinstance(
463
            definition.singleton_type, Alias
464 1
        ):
465
            raise InvalidDependencyDefinition(
466 1
                "Aliases are not valid inside singletons in an explicit container"
467
            )
468
        return definition
469
470 1
    def clone(self):
471
        """returns a copy of the container
472
        :return:
473 1
        """
474
        return ExplicitContainer(self, log_undefined_deps=self._undefined_logger)
475
476
477
class EmptyDefinitionSet(DefinitionsSource):
478
    """
479 1
    Represents the starting state for a collection of dependency definitions
480
    i.e. None and everything has to be built with reflection
481
    """
482
483
    def get_definition(self, dep_type: Type[X]) -> Optional[SpecialDepDefinition[X]]:
484
        """
485 1
        No types are defined in the empty set
486
        :param dep_type:
487 1
        :return:
488 1
        """
489 1
        return None
490
491
    @property
492 1
    def defined_types(self) -> Set[Type]:
493 1
        return set()
494
495 1
496
def _update_nothing(_c: WriteableContainer, _a: typing.Collection, _k: Dict):
497
    return None
498