Test Failed
Pull Request — master (#252)
by Steve
06:28
created

lagom.container   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 520
Duplicated Lines 0 %

Test Coverage

Coverage 98.86%

Importance

Changes 0
Metric Value
eloc 284
dl 0
loc 520
rs 4.5599
c 0
b 0
f 0
ccs 174
cts 176
cp 0.9886
wmc 58

24 Methods

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