This repository has been archived by the owner on Jun 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnamed_exit_stack.py
117 lines (89 loc) · 3.71 KB
/
named_exit_stack.py
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
""" An ExitStack that allows you to de-initialize objects sequentially and catch errors """
from __future__ import annotations
import sys
import logging
from typing import TypeVar
from typing import ContextManager, AsyncContextManager
class _NamedExitStackBase:
""" Features common to both sync and async stacks """
def __init__(self):
self._stack = {}
def has_context(self, name: str) -> bool:
""" Check if the context has been entered and is still open """
return name in self._stack
@property
def properly_closed(self):
""" Is the stack closed? That is, are there no remaining contexts to close? """
return not bool(self._stack)
class NamedExitStack(_NamedExitStackBase):
""" Exit stack with named context managers
Why? Because the standard contextlib.ExitStack actually loses errors. In case of multiple clean-up errors,
it only raises the first error, and all other errors go unreported.
This is unacceptable in our case because we want to log them!
With this context manager, you can exit them one by one and catch every error.
Example:
try:
stack = NamedExitStack()
stack.enter_context('db', ...)
stack.enter_context('redis', ...)
except:
stack.emergency_exit_all_context_and_log(logger)
...
for name in ['db', 'redis']:
try:
stack.exit_context('db')
except:
logger.exception(...)
assert stack.properly_closed()
"""
_stack: dict[str, ContextManager]
__slots__ = '_stack'
def enter_context(self, name: str, cm: ContextManager[T]) -> T:
""" Enter a context, give it a name """
self._stack[name] = cm
return cm.__enter__()
def exit_context(self, name: str):
""" Exit the context specified by name, if exists
If the context has not been entered, no error is raised:
this behavior allows to exit contexts without checking whether they've been entered.
"""
# Get the context manager
cm = self._stack.pop(name, None)
# Exit it, if found
if cm is not None:
cm.__exit__(*sys.exc_info())
def emergency_exit_all_context_and_log(self, logger: logging.Logger) -> list[Exception]:
""" Exit all context managers; don't raise errors, but log them and return them """
errors = []
while self._stack:
name, cm = self._stack.popitem()
try:
cm.__exit__(*sys.exc_info())
except Exception as e:
logger.exception(f'Clean-up error for {name!r}')
errors.append(e)
return errors
class NamedAsyncExitStack(_NamedExitStackBase):
""" Exit stack for async named context managers """
_stack: dict[str, AsyncContextManager]
__slots__ = '_stack'
async def enter_async_context(self, name: str, cm: AsyncContextManager[T]) -> T:
""" Enter a context, give it a name """
self._stack[name] = cm
return await cm.__aenter__()
async def exit_async_context(self, name: str):
""" Exit the context specified by name, if exists """
cm = self._stack.pop(name, None)
if cm is not None:
await cm.__aexit__(*sys.exc_info())
async def emergency_exit_all_async_contexts_and_log(self, logger: logging.Logger) -> list[Exception]:
errors = []
while self._stack:
name, cm = self._stack.popitem()
try:
await cm.__aexit__(*sys.exc_info())
except Exception as e:
logger.exception(f'Clean-up error for {name!r}')
errors.append(e)
return errors
T = TypeVar('T')