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

lagom.context_based._ContextBoundFunction.rebind()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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