Passed
Pull Request — master (#226)
by Steve
03:21
created

lagom.context_based   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 98.55%

Importance

Changes 0
Metric Value
eloc 112
dl 0
loc 185
ccs 68
cts 69
cp 0.9855
rs 10
c 0
b 0
f 0
wmc 20

12 Methods

Rating   Name   Duplication   Size   Complexity  
A _ContextBoundFunction.rebind() 0 4 1
A ContextContainer.partial() 0 11 1
A ContextContainer._context_type_def() 0 14 4
A ContextContainer.magic_partial() 0 13 1
A _ContextBoundFunction.__init__() 0 7 1
A ContextContainer._context_resolver() 0 9 1
A ContextContainer.__enter__() 0 15 4
A ContextContainer.__exit__() 0 4 2
A ContextContainer.__init__() 0 10 1
A ContextContainer._singleton_type_def() 0 5 1
A ContextContainer.clone() 0 9 1
A _ContextBoundFunction.__call__() 0 3 2
1 1
import logging
2 1
from contextlib import ExitStack
3 1
from copy import copy
4 1
from functools import wraps
5 1
from typing import (
6
    Collection,
7
    Union,
8
    Type,
9
    TypeVar,
10
    Optional,
11
    cast,
12
    ContextManager,
13
    Iterator,
14
    Generator,
15
    Callable,
16
    List,
17
)
18
19 1
from lagom import Container
20 1
from lagom.compilaton import mypyc_attr
21 1
from lagom.definitions import ConstructionWithContainer, SingletonWrapper, Alias
22 1
from lagom.exceptions import InvalidDependencyDefinition
23 1
from lagom.interfaces import (
24
    ReadableContainer,
25
    SpecialDepDefinition,
26
    CallTimeContainerUpdate,
27
    ContainerBoundFunction,
28
)
29
30 1
X = TypeVar("X")
31
32
33 1
class _ContextBoundFunction(ContainerBoundFunction[X]):
34
    """
35
    Represents an instance of a function bound to a context container
36
    """
37
38 1
    context_container: "ContextContainer"
39 1
    partially_bound_function: ContainerBoundFunction
40
41 1
    def __init__(
42
        self,
43
        context_container: "ContextContainer",
44
        partially_bound_function: ContainerBoundFunction,
45
    ):
46 1
        self.context_container = context_container
47 1
        self.partially_bound_function = partially_bound_function
48
49 1
    def __call__(self, *args, **kwargs) -> X:
50 1
        with self.context_container as c:
51 1
            return self.partially_bound_function.rebind(c)(*args, **kwargs)
52
53 1
    def rebind(self, container: ReadableContainer) -> "ContainerBoundFunction[X]":
54
        return wraps(self.partially_bound_function)(
55
            _ContextBoundFunction(
56
                self.context_container, self.partially_bound_function.rebind(container)
57
            )
58
        )
59
60
61 1
@mypyc_attr(allow_interpreted_subclasses=True)
62 1
class ContextContainer(Container):
63
    """
64
    Wraps a regular container but is a ContextManager for use within a `with`.
65
66
    >>> from tests.examples import SomeClass, SomeClassManager
67
    >>> from lagom import Container
68
    >>> from typing import ContextManager
69
    >>>
70
    >>> # The regular container
71
    >>> c = Container()
72
    >>>
73
    >>> # register a context manager for SomeClass
74
    >>> c[ContextManager[SomeClass]] = SomeClassManager
75
    >>>
76
    >>> context_c = ContextContainer(c, context_types=[SomeClass])
77
    >>> with context_c as c:
78
    ...     c[SomeClass]
79
    <tests.examples.SomeClass object at ...>
80
    """
81
82 1
    exit_stack: Optional[ExitStack] = None
83 1
    _context_types: Collection[Type]
84 1
    _context_singletons: Collection[Type]
85
86 1
    def __init__(
87
        self,
88
        container: Container,
89
        context_types: Collection[Type],
90
        context_singletons: Collection[Type] = tuple(),
91
        log_undefined_deps: Union[bool, logging.Logger] = False,
92
    ):
93 1
        self._context_types = context_types
94 1
        self._context_singletons = context_singletons
95 1
        super().__init__(container, log_undefined_deps)
96
97 1
    def clone(self) -> "ContextContainer":
98
        """returns a copy of the container
99
        :return:
100
        """
101 1
        return ContextContainer(
102
            self,
103
            context_types=self._context_types,
104
            context_singletons=self._context_singletons,
105
            log_undefined_deps=self._undefined_logger,
106
        )
107
108 1
    def __enter__(self):
109 1
        if not self.exit_stack:
110
            # All actual context definitions happen on a clone so that there's isolation between invocations
111 1
            in_context = self.clone()
112 1
            for dep_type in set(self._context_types):
113 1
                in_context[dep_type] = self._context_type_def(dep_type)
114 1
            for dep_type in set(self._context_singletons):
115 1
                in_context[dep_type] = self._singleton_type_def(dep_type)
116 1
            in_context.exit_stack = ExitStack()
117
118
            # The parent context manager keeps track of the inner clone
119 1
            self.exit_stack = ExitStack()
120 1
            self.exit_stack.enter_context(in_context)
121 1
            return in_context
122 1
        return self
123
124 1
    def __exit__(self, exc_type, exc_val, exc_tb):
125 1
        if self.exit_stack:
126 1
            self.exit_stack.close()
127 1
            self.exit_stack = None
128
129 1
    def partial(
130
        self,
131
        func: Callable[..., X],
132
        shared: Optional[List[Type]] = None,
133
        container_updater: Optional[CallTimeContainerUpdate] = None,
134
    ) -> ContainerBoundFunction[X]:
135 1
        base_partial = super(ContextContainer, self).partial(
136
            func, shared, container_updater
137
        )
138
139 1
        return wraps(base_partial)(_ContextBoundFunction(self, base_partial))
140
141 1
    def magic_partial(
142
        self,
143
        func: Callable[..., X],
144
        shared: Optional[List[Type]] = None,
145
        keys_to_skip: Optional[List[str]] = None,
146
        skip_pos_up_to: int = 0,
147
        container_updater: Optional[CallTimeContainerUpdate] = None,
148
    ) -> ContainerBoundFunction[X]:
149 1
        base_partial = super(ContextContainer, self).magic_partial(
150
            func, shared, keys_to_skip, skip_pos_up_to, container_updater
151
        )
152
153 1
        return wraps(base_partial)(_ContextBoundFunction(self, base_partial))
154
155 1
    def _context_type_def(self, dep_type: Type):
156 1
        type_def = self.get_definition(ContextManager[dep_type]) or self.get_definition(Iterator[dep_type]) or self.get_definition(Generator[dep_type, None, None])  # type: ignore
157 1
        if type_def is None:
158 1
            raise InvalidDependencyDefinition(
159
                f"A ContextManager[{dep_type}] should be defined. "
160
                f"This could be an Iterator[{dep_type}] or Generator[{dep_type}, None, None] "
161
                f"with the @contextmanager decorator"
162
            )
163 1
        if isinstance(type_def, Alias):
164
            # Without this we create a definition that points to
165
            # itself.
166 1
            type_def = copy(type_def)
167 1
            type_def.skip_definitions = True
168 1
        return ConstructionWithContainer(lambda c: self._context_resolver(c, type_def))  # type: ignore
169
170 1
    def _singleton_type_def(self, dep_type: Type):
171
        """
172
        The same as context_type_def but acts as a singleton within this container
173
        """
174 1
        return SingletonWrapper(self._context_type_def(dep_type))
175
176 1
    def _context_resolver(self, c: ReadableContainer, type_def: SpecialDepDefinition):
177
        """
178
        Takes an existing definition which must be a context manager. Returns
179
        the value of the context manager from __enter__ and then places the
180
        __exit__ in this container's exit stack
181
        """
182 1
        assert self.exit_stack, "Types can only be resolved within a with"
183 1
        context_manager = cast(ContextManager, type_def.get_instance(c))
184
        return self.exit_stack.enter_context(context_manager)
185