File: contextmanagers.rst

package info (click to toggle)
python-anyio 4.11.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,292 kB
  • sloc: python: 16,605; sh: 21; makefile: 9
file content (164 lines) | stat: -rw-r--r-- 5,941 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
Context manager mix-in classes
==============================

.. py:currentmodule:: anyio

Python classes that want to offer context management functionality normally implement
``__enter__()`` and ``__exit__()`` (for synchronous context managers) or
``__aenter__()`` and ``__aexit__()`` (for asynchronous context managers). While this
offers precise control and re-entrancy support, embedding _other_ context managers in
this logic can be very error prone. To make this easier, AnyIO provides two context
manager mix-in classes, :class:`ContextManagerMixin` and
:class:`AsyncContextManagerMixin`. These classes provide implementations of
``__enter__()``, ``__exit__()`` or ``__aenter__()``, and ``__aexit__()``, that provide
another way to implement context managers similar to
:func:`@contextmanager <contextlib.contextmanager>` and
:func:`@asynccontextmanager <contextlib.asynccontextmanager>` - a generator-based
approach where the ``yield`` statement signals that the context has been entered.

Here's a trivial example of how to use the mix-in classes:

.. tabs::

   .. code-tab:: python Synchronous

      from collections.abc import Generator
      from contextlib import contextmanager
      from typing import Self

      from anyio import ContextManagerMixin

      class MyContextManager(ContextManagerMixin):
          @contextmanager
          def __contextmanager__(self) -> Generator[Self]:
              print("entering context")
              yield self
              print("exiting context")

   .. code-tab:: python Asynchronous

      from collections.abc import AsyncGenerator
      from contextlib import asynccontextmanager
      from typing import Self

      from anyio import AsyncContextManagerMixin

      class MyAsyncContextManager(AsyncContextManagerMixin):
          @asynccontextmanager
          async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
              print("entering context")
              yield self
              print("exiting context")

When should I use the contextmanager mix-in classes?
----------------------------------------------------

When embedding other context managers, a common mistake is forgetting about error
handling when entering the context. Consider this example::

    from typing import Self

    from anyio import create_task_group

    class MyBrokenContextManager:
        async def __aenter__(self) -> Self:
            self._task_group = await create_task_group().__aenter__()
            # BOOM: missing the "arg" argument here to my_background_func!
            self._task_group.start_soon(self.my_background_func)
            return self

        async def __aexit__(self, exc_type, exc_value, traceback) -> bool | None:
            return await self._task_group.__aexit__(exc_type, exc_value, traceback)

        async my_background_func(self, arg: int) -> None:
            ...

It's easy to think that you have everything covered with ``__aexit__()`` here, but what
if something goes wrong in ``__aenter__()``?  The ``__aexit__()`` method will never be
called.

The mix-in classes solve this problem by providing a robust implementation of
``__enter__()``/``__exit__()`` or ``__aenter__()``/``__aexit__()`` that handles errors
correctly. Thus, the above code should be written as::

    from collections.abc import AsyncGenerator
    from contextlib import asynccontextmanager
    from typing import Self

    from anyio import AsyncContextManagerMixin, create_task_group

    class MyBetterContextManager(AsyncContextManagerMixin):
        @asynccontextmanager
        async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
            async with create_task_group() as task_group:
                # Still crashes, but at least now the task group is exited
                task_group.start_soon(self.my_background_func)
                yield self

        async my_background_func(self, arg: int) -> None:
            ...

.. seealso:: :ref:`cancel_scope_stack_corruption`

Inheriting context manager classes
----------------------------------

Here's how you would call the superclass implementation from a subclass:

.. tabs::

   .. code-tab:: python Synchronous

      from collections.abc import Generator
      from contextlib import contextmanager
      from typing import Self

      from anyio import ContextManagerMixin

      class SuperclassContextManager(ContextManagerMixin):
          @contextmanager
          def __contextmanager__(self) -> Generator[Self]:
              print("superclass entered")
              try:
                  yield self
              finally:
                  print("superclass exited")


      class SubclassContextManager(SuperclassContextManager):
          @contextmanager
          def __contextmanager__(self) -> Generator[Self]:
              print("subclass entered")
              try:
                  with super().__contextmanager__():
                      yield self
              finally:
                  print("subclass exited")

   .. code-tab:: python Asynchronous

      from collections.abc import AsyncGenerator
      from contextlib import asynccontextmanager
      from typing import Self

      from anyio import AsyncContextManagerMixin

      class SuperclassContextManager(AsyncContextManagerMixin):
          @asynccontextmanager
          async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
              print("superclass entered")
              try:
                  yield self
              finally:
                  print("superclass exited")


      class SubclassContextManager(SuperclassContextManager):
          @asynccontextmanager
          async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
              print("subclass entered")
              try:
                  async with super().__asynccontextmanager__():
                      yield self
              finally:
                  print("subclass exited")