Passed
Pull Request — master (#226)
by Steve
02:59
created

lagom.context_based._ContextBoundFunction.rebind()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

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