FastApiIntegration.override_for_test()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nop 1
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
"""
2
FastAPI (https://fastapi.tiangolo.com/)
3
4
"""
5
6 1
from contextlib import contextmanager
7 1
from typing import TypeVar, Optional, Type, List, Iterator
8
9 1
from fastapi import Depends
10 1
from starlette.requests import Request
11
12 1
from ..context_based import ContextContainer
13 1
from ..definitions import PlainInstance
14 1
from ..interfaces import ExtendableContainer, ReadableContainer, WriteableContainer
15 1
from ..updaters import update_container_singletons
16
17 1
T = TypeVar("T")
18
19
20 1
class FastApiIntegration:
21
    """
22
    Integration between a container and the FastAPI framework.
23
    Provides a method `Depends` which functions in the same way as
24
    FastApi `Depends`
25
    """
26
27 1
    _container: ExtendableContainer
28
29 1
    def __init__(
30
        self,
31
        container: ExtendableContainer,
32
        request_singletons: Optional[List[Type]] = None,
33
        request_context_singletons: Optional[List[Type]] = None,
34
    ):
35 1
        self._container = container
36 1
        self._request_singletons = request_singletons or []
37 1
        self._request_context_singletons = request_context_singletons or []
38
39 1
    def depends(self, dep_type: Type[T]) -> T:
40
        """Returns a Depends object which FastAPI understands
41
42
        :param dep_type:
43
        :return:
44
        """
45
46 1
        def _container_from_request(request: Request) -> Iterator[ReadableContainer]:
47
            """
48
            We use the state of the request object to store a single instance of the
49
            container. Request level singletons can then be defined on this container.
50
            We only need to construct it once per request. This container is also
51
            wrapped in a ContextContainer which is yielded to fastapi and can call
52
            the __exit__ methods of any context managers used constructing objects
53
            during the requests lifetime.
54
            """
55 1
            if (
56
                not hasattr(request.state, "lagom_request_container")
57
                or not request.state.lagom_request_container
58
            ):
59 1
                request.state.lagom_request_container = self._build_container(request)
60 1
                with request.state.lagom_request_container as c:
61 1
                    yield c
62
            else:
63
                # No need to "with" as it's already been done once and this
64
                # will handle the exit
65 1
                yield request.state.lagom_request_container
66
67 1
        def _resolver(
68
            container: ExtendableContainer = Depends(_container_from_request),
69
        ):
70 1
            return container.resolve(dep_type)
71
72 1
        return Depends(_resolver)
73
74 1
    @contextmanager
75 1
    def override_for_test(self) -> Iterator[WriteableContainer]:
76
        """
77
        Returns a ContextManager that returns an editable container
78
        that will temporarily alter the dependency injection resolution
79
        of all dependencies bound to this container.
80
81
            client = TestClient(app)
82
            with deps.override_for_test() as test_container:
83
                # FooService is an external API so mock it during test
84
                test_container[FooService] = Mock(FooService)
85
                response = client.get("/")
86
            assert response.status_code == 200
87
88
        :return:
89
        """
90 1
        original = self._container
91 1
        new_container_for_test = self._container.clone()
92 1
        self._container = new_container_for_test  # type: ignore
93 1
        try:
94 1
            yield new_container_for_test
95
        finally:
96 1
            self._container = original
97
98 1
    def _build_container(self, request: Request) -> ContextContainer:
99 1
        container = self._container.clone()
100 1
        container.define(Request, PlainInstance(request))
101 1
        request_container = update_container_singletons(
102
            container, self._request_singletons
103
        )
104 1
        return ContextContainer(
105
            request_container,
106
            context_types=[],
107
            context_singletons=self._request_context_singletons,
108
        )
109