asyncio-for-robotics 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asyncio_for_robotics/__init__.py +15 -0
- asyncio_for_robotics/core/__init__.py +15 -0
- asyncio_for_robotics/core/_compat.py +13 -0
- asyncio_for_robotics/core/_logger.py +125 -0
- asyncio_for_robotics/core/scope.py +379 -0
- asyncio_for_robotics/core/sub.py +382 -0
- asyncio_for_robotics/core/utils.py +161 -0
- asyncio_for_robotics/example/__init__.py +0 -0
- asyncio_for_robotics/example/custom_stdout.py +44 -0
- asyncio_for_robotics/example/delete_me.py +32 -0
- asyncio_for_robotics/example/python_discussion.py +116 -0
- asyncio_for_robotics/example/ros2_discussion.py +113 -0
- asyncio_for_robotics/example/ros2_double_listener.py +78 -0
- asyncio_for_robotics/example/ros2_double_talker.py +108 -0
- asyncio_for_robotics/example/ros2_event_callback.py +153 -0
- asyncio_for_robotics/example/ros2_listener.py +24 -0
- asyncio_for_robotics/example/ros2_pubsub.py +58 -0
- asyncio_for_robotics/example/ros2_service_client.py +33 -0
- asyncio_for_robotics/example/ros2_service_server.py +29 -0
- asyncio_for_robotics/example/ros2_talker.py +35 -0
- asyncio_for_robotics/example/zenoh_discussion.py +68 -0
- asyncio_for_robotics/py.typed +1 -0
- asyncio_for_robotics/ros2/__init__.py +46 -0
- asyncio_for_robotics/ros2/future.py +47 -0
- asyncio_for_robotics/ros2/service.py +317 -0
- asyncio_for_robotics/ros2/session.py +106 -0
- asyncio_for_robotics/ros2/session_types.py +281 -0
- asyncio_for_robotics/ros2/sub.py +107 -0
- asyncio_for_robotics/ros2/utils.py +45 -0
- asyncio_for_robotics/textio/__init__.py +12 -0
- asyncio_for_robotics/textio/sub.py +117 -0
- asyncio_for_robotics/zenoh/__init__.py +33 -0
- asyncio_for_robotics/zenoh/session.py +113 -0
- asyncio_for_robotics/zenoh/sub.py +75 -0
- asyncio_for_robotics-1.4.0.dist-info/METADATA +283 -0
- asyncio_for_robotics-1.4.0.dist-info/RECORD +39 -0
- asyncio_for_robotics-1.4.0.dist-info/WHEEL +5 -0
- asyncio_for_robotics-1.4.0.dist-info/licenses/LICENSE +21 -0
- asyncio_for_robotics-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .core.scope import AUTO_SCOPE, Scope, ScopeBreak, scoped
|
|
2
|
+
from .core.utils import soft_timeout, soft_wait_for, Rate
|
|
3
|
+
from .core.sub import BaseSub, ConverterSub
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AUTO_SCOPE",
|
|
7
|
+
"Scope",
|
|
8
|
+
"ScopeBreak",
|
|
9
|
+
"scoped",
|
|
10
|
+
"soft_wait_for",
|
|
11
|
+
"soft_timeout",
|
|
12
|
+
"Rate",
|
|
13
|
+
"BaseSub",
|
|
14
|
+
"ConverterSub",
|
|
15
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .scope import AUTO_SCOPE, Scope, ScopeBreak, scoped
|
|
2
|
+
from .utils import soft_timeout, soft_wait_for, Rate
|
|
3
|
+
from .sub import BaseSub, ConverterSub
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AUTO_SCOPE",
|
|
7
|
+
"Scope",
|
|
8
|
+
"ScopeBreak",
|
|
9
|
+
"scoped",
|
|
10
|
+
"BaseSub",
|
|
11
|
+
"ConverterSub",
|
|
12
|
+
"soft_wait_for",
|
|
13
|
+
"soft_timeout",
|
|
14
|
+
"Rate",
|
|
15
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from asyncio import TaskGroup
|
|
3
|
+
except ImportError:
|
|
4
|
+
try:
|
|
5
|
+
from taskgroup import TaskGroup
|
|
6
|
+
except ImportError:
|
|
7
|
+
TaskGroup = NotImplemented
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
BaseExceptionGroup = BaseExceptionGroup
|
|
11
|
+
except NameError:
|
|
12
|
+
# BaseExceptionGroup = NotImplemented
|
|
13
|
+
from exceptiongroup import BaseExceptionGroup
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import copy
|
|
3
|
+
import logging.config
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import colorama
|
|
9
|
+
from colorama import Fore, Style, init
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsonLineFormatter(logging.Formatter):
|
|
13
|
+
"""Outputs log records as JSON lines."""
|
|
14
|
+
|
|
15
|
+
def format(self, record):
|
|
16
|
+
log_record = {
|
|
17
|
+
"time": datetime.fromtimestamp(record.created).isoformat(),
|
|
18
|
+
"level": record.levelname,
|
|
19
|
+
"logger": record.name,
|
|
20
|
+
"message": record.getMessage(),
|
|
21
|
+
"module": record.module,
|
|
22
|
+
"funcName": record.funcName,
|
|
23
|
+
"line": record.lineno,
|
|
24
|
+
}
|
|
25
|
+
if record.exc_info:
|
|
26
|
+
log_record["exc_info"] = self.formatException(record.exc_info)
|
|
27
|
+
return json.dumps(log_record)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LEVEL_COLORS = {
|
|
31
|
+
"DEBUG": Fore.BLUE,
|
|
32
|
+
"INFO": Fore.CYAN,
|
|
33
|
+
"WARNING": Fore.YELLOW,
|
|
34
|
+
"ERROR": Fore.RED,
|
|
35
|
+
"CRITICAL": Fore.BLACK + Style.BRIGHT + colorama.Back.RED,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OnlyLevelFilter(logging.Filter):
|
|
40
|
+
"""Only allow a single level through this handler."""
|
|
41
|
+
def __init__(self, level):
|
|
42
|
+
self.level = level
|
|
43
|
+
def filter(self, record):
|
|
44
|
+
return record.levelno == self.level
|
|
45
|
+
|
|
46
|
+
class ColoredFormatter(logging.Formatter):
|
|
47
|
+
"""Adds colors to levelname in logs."""
|
|
48
|
+
|
|
49
|
+
def format(self, record: logging.LogRecord):
|
|
50
|
+
record = copy.deepcopy(record)
|
|
51
|
+
levelname = record.levelname
|
|
52
|
+
if levelname in LEVEL_COLORS:
|
|
53
|
+
record.levelname = (
|
|
54
|
+
f"{LEVEL_COLORS[levelname]}{record.levelname[0]}{Style.RESET_ALL}"
|
|
55
|
+
)
|
|
56
|
+
record.msg = f"{LEVEL_COLORS[levelname]}{record.msg}{Style.RESET_ALL}"
|
|
57
|
+
return super().format(record)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_logger(debug_path: Optional[str] = None):
|
|
61
|
+
handlers = ["stdout", "stderr"]
|
|
62
|
+
if debug_path:
|
|
63
|
+
handlers.append("json")
|
|
64
|
+
handlers.append("userlog")
|
|
65
|
+
cfg = {
|
|
66
|
+
"version": 1,
|
|
67
|
+
"disable_existing_loggers": False,
|
|
68
|
+
"formatters": {
|
|
69
|
+
"user": {
|
|
70
|
+
"()": ColoredFormatter,
|
|
71
|
+
"format": "%(levelname)s| %(message)s",
|
|
72
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S",
|
|
73
|
+
},
|
|
74
|
+
"json": {
|
|
75
|
+
"()": JsonLineFormatter, # custom formatter
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"filters": {"only_info": {"()": OnlyLevelFilter, "level": logging.INFO}},
|
|
79
|
+
"handlers": {
|
|
80
|
+
"stdout": {
|
|
81
|
+
"class": "logging.StreamHandler",
|
|
82
|
+
"level": "INFO",
|
|
83
|
+
"formatter": "user",
|
|
84
|
+
"filters": ["only_info"],
|
|
85
|
+
"stream": "ext://sys.stdout",
|
|
86
|
+
},
|
|
87
|
+
"stderr": {
|
|
88
|
+
"class": "logging.StreamHandler",
|
|
89
|
+
"level": "WARNING",
|
|
90
|
+
"formatter": "user",
|
|
91
|
+
"stream": "ext://sys.stderr",
|
|
92
|
+
},
|
|
93
|
+
"json": {
|
|
94
|
+
"class": "logging.FileHandler",
|
|
95
|
+
"level": "DEBUG",
|
|
96
|
+
"formatter": "json",
|
|
97
|
+
"filename": (
|
|
98
|
+
os.path.join(os.path.expanduser(debug_path) , "debug.log.jsonl")
|
|
99
|
+
if debug_path is not None
|
|
100
|
+
else "log.jsonl"
|
|
101
|
+
), # path relative to working dir
|
|
102
|
+
"mode": "w",
|
|
103
|
+
},
|
|
104
|
+
"userlog": {
|
|
105
|
+
"class": "logging.FileHandler",
|
|
106
|
+
"level": "DEBUG",
|
|
107
|
+
"formatter": "user",
|
|
108
|
+
"filename": (
|
|
109
|
+
os.path.join(os.path.expanduser(debug_path) , "debug.log")
|
|
110
|
+
if debug_path is not None
|
|
111
|
+
else "log"
|
|
112
|
+
), # path relative to working dir
|
|
113
|
+
"mode": "w",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"loggers": {
|
|
117
|
+
"asyncio_for_robotics": {
|
|
118
|
+
"level": "DEBUG",
|
|
119
|
+
"handlers": handlers,
|
|
120
|
+
"propagate": False,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
logging.config.dictConfig(cfg)
|
|
125
|
+
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from contextlib import AsyncExitStack
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, Coroutine, Optional, ParamSpec, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from ._compat import BaseExceptionGroup, TaskGroup
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_MISSING = object()
|
|
15
|
+
_P = ParamSpec("_P")
|
|
16
|
+
_R = TypeVar("_R")
|
|
17
|
+
_CURRENT_SCOPE: contextvars.ContextVar["Scope | None"] = contextvars.ContextVar(
|
|
18
|
+
"afor_current_scope",
|
|
19
|
+
default=None,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ScopeBreak(Exception):
|
|
24
|
+
"""Exit the current afor.Scope immediately.
|
|
25
|
+
|
|
26
|
+
Raise this inside the body of the current scope when you want to stop that
|
|
27
|
+
scope now and jump to after the ``async with`` block.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _ScopeCancelled(Exception):
|
|
32
|
+
"""Internal exception used to stop a scope early."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _AutoScope:
|
|
36
|
+
"""Sentinel type for :data:`AUTO_SCOPE`.
|
|
37
|
+
|
|
38
|
+
Instances are never entered, attached to, or otherwise used as a real
|
|
39
|
+
scope: the identity check ``scope is AUTO_SCOPE`` is the only
|
|
40
|
+
interaction. All scope-shaped operations raise ``RuntimeError`` so
|
|
41
|
+
accidental use fails loudly.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return "AUTO_SCOPE"
|
|
46
|
+
|
|
47
|
+
def _fail(self) -> None:
|
|
48
|
+
raise RuntimeError(
|
|
49
|
+
"AUTO_SCOPE is a sentinel, not a usable scope. "
|
|
50
|
+
"Pass a real afor.Scope, or None to opt out of auto-attach."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def __aenter__(self):
|
|
54
|
+
self._fail()
|
|
55
|
+
|
|
56
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
57
|
+
self._fail()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
#: Sentinel for ``scope=`` parameters.
|
|
61
|
+
#:
|
|
62
|
+
#: When passed as ``scope=AUTO_SCOPE`` (the default on every subscriber),
|
|
63
|
+
#: the resource attaches to :meth:`Scope.current` if one is active, or
|
|
64
|
+
#: runs unattached otherwise. Pass ``None`` to opt out explicitly, or a
|
|
65
|
+
#: concrete :class:`Scope` to attach to a specific one.
|
|
66
|
+
AUTO_SCOPE: "Scope" = cast("Scope", _AutoScope())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Scope:
|
|
70
|
+
"""Lexical owner for async resources and background tasks.
|
|
71
|
+
|
|
72
|
+
A scope binds a ``TaskGroup`` and an ``AsyncExitStack`` together: resources
|
|
73
|
+
created inside the scope auto-attach to it and are cleaned up when the
|
|
74
|
+
scope exits. If any task fails, the scope cancels the others and
|
|
75
|
+
propagates the error.
|
|
76
|
+
|
|
77
|
+
Use as a context manager or as a decorator::
|
|
78
|
+
|
|
79
|
+
async with afor.Scope() as scope:
|
|
80
|
+
sub = Sub(...) # auto-attaches
|
|
81
|
+
scope.task_group.create_task(background_work())
|
|
82
|
+
|
|
83
|
+
@afor.scoped
|
|
84
|
+
async def main():
|
|
85
|
+
sub = Sub(...) # same auto-attach, less indentation
|
|
86
|
+
|
|
87
|
+
``raise afor.ScopeBreak()`` exits the current scope immediately (like
|
|
88
|
+
``break`` for an ``async with`` block).
|
|
89
|
+
|
|
90
|
+
Advanced users can access ``scope.task_group`` and ``scope.exit_stack``
|
|
91
|
+
directly for custom structured-concurrency or cleanup needs. The
|
|
92
|
+
``scope.finished`` future lets external code observe when teardown
|
|
93
|
+
completes.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
#: Underlying AsyncExitStack.
|
|
98
|
+
#: It is None before ``__aenter__`` and after ``__aexit__``.
|
|
99
|
+
self._exit_stack: Optional[AsyncExitStack] = None
|
|
100
|
+
#: Underlying TaskGroup.
|
|
101
|
+
#: It is None before ``__aenter__`` and after ``__aexit__``.
|
|
102
|
+
self._task_group: Optional[TaskGroup] = None
|
|
103
|
+
self._finished: Optional[asyncio.Future[None]] = None
|
|
104
|
+
self._updating_finished = False
|
|
105
|
+
self._current_scope_token: Optional[contextvars.Token["Scope | None"]] = None
|
|
106
|
+
self._cancel_called = False
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def exit_stack(self) -> AsyncExitStack:
|
|
110
|
+
"""Underlying AsyncExitStack.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Underlying AsyncExitStack.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
RuntimeError: if called when the scope is not active.
|
|
117
|
+
"""
|
|
118
|
+
if self._exit_stack is None:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
"Cannot get the AsyncExitStack of a afor.Scope that is not active:"
|
|
121
|
+
"not entered yet; or was exited."
|
|
122
|
+
)
|
|
123
|
+
return self._exit_stack
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def task_group(self) -> TaskGroup:
|
|
127
|
+
"""Underlying TaskGroup.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Underlying TaskGroup.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
RuntimeError: if called when the scope is not active.
|
|
134
|
+
"""
|
|
135
|
+
if self._task_group is None:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"Cannot get the TaskGroup of a afor.Scope that is not active:"
|
|
138
|
+
"not entered yet; or was exited."
|
|
139
|
+
)
|
|
140
|
+
return self._task_group
|
|
141
|
+
|
|
142
|
+
async def __aenter__(self) -> "Scope":
|
|
143
|
+
"""Enter the scope and activate its TaskGroup and AsyncExitStack."""
|
|
144
|
+
logger.debug("Entered scope block")
|
|
145
|
+
self._finished = asyncio.get_running_loop().create_future()
|
|
146
|
+
self._finished.add_done_callback(self._finished_done_callback)
|
|
147
|
+
self._finished.add_done_callback(self._finished_consume_exception_callback)
|
|
148
|
+
self._exit_stack = AsyncExitStack()
|
|
149
|
+
await self._exit_stack.__aenter__()
|
|
150
|
+
self._exit_stack.callback(self._cancel_tg)
|
|
151
|
+
self._task_group = await self._exit_stack.enter_async_context(TaskGroup())
|
|
152
|
+
self._current_scope_token = _CURRENT_SCOPE.set(self)
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
156
|
+
"""Leave the scope, running cleanup callbacks and exiting the TaskGroup.
|
|
157
|
+
|
|
158
|
+
``ScopeBreak`` is treated as normal scoped control flow and is
|
|
159
|
+
suppressed after teardown finishes.
|
|
160
|
+
"""
|
|
161
|
+
logger.debug("Exiting scope block")
|
|
162
|
+
assert self._exit_stack is not None
|
|
163
|
+
assert self._current_scope_token is not None
|
|
164
|
+
_CURRENT_SCOPE.reset(self._current_scope_token)
|
|
165
|
+
try:
|
|
166
|
+
self.cancel()
|
|
167
|
+
suppressed = await self._exit_stack.__aexit__(exc_type, exc, tb)
|
|
168
|
+
self._set_finished_result()
|
|
169
|
+
return suppressed or exc_type is ScopeBreak
|
|
170
|
+
except BaseException as err:
|
|
171
|
+
if exc_type is ScopeBreak:
|
|
172
|
+
filtered_break = self._strip_scope_break(err)
|
|
173
|
+
if filtered_break is None:
|
|
174
|
+
self._set_finished_cancelled()
|
|
175
|
+
return True
|
|
176
|
+
err = filtered_break
|
|
177
|
+
filtered = self._strip_scope_cancel(err)
|
|
178
|
+
if filtered is None:
|
|
179
|
+
if exc_type is asyncio.CancelledError:
|
|
180
|
+
self._set_finished_cancelled()
|
|
181
|
+
else:
|
|
182
|
+
self._set_finished_result()
|
|
183
|
+
return exc_type in (None, asyncio.CancelledError)
|
|
184
|
+
self._set_finished_exception(filtered)
|
|
185
|
+
raise filtered
|
|
186
|
+
finally:
|
|
187
|
+
self._task_group = None
|
|
188
|
+
self._exit_stack = None
|
|
189
|
+
logger.debug("Finished scope block")
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def current(cls, default: Any = _MISSING) -> "Scope":
|
|
193
|
+
"""Return the current lexical afor scope.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
default:
|
|
197
|
+
Value returned when no scope is active.
|
|
198
|
+
If omitted, a RuntimeError is raised instead.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The currently active scope for this execution context.
|
|
202
|
+
"""
|
|
203
|
+
scope = _CURRENT_SCOPE.get()
|
|
204
|
+
if scope is None:
|
|
205
|
+
if default is _MISSING:
|
|
206
|
+
raise RuntimeError("No active afor.Scope")
|
|
207
|
+
return default
|
|
208
|
+
return scope
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def finished(self) -> asyncio.Future[None]:
|
|
212
|
+
"""Future resolved when scope teardown completes.
|
|
213
|
+
|
|
214
|
+
This future is available after the scope has been entered.
|
|
215
|
+
Typical uses:
|
|
216
|
+
- inspect whether another scope finished cleanly
|
|
217
|
+
- await teardown after requesting ``scope.cancel()`` elsewhere
|
|
218
|
+
|
|
219
|
+
It is usually not useful to await ``finished`` from inside the same
|
|
220
|
+
scope body unless some other task is responsible for ending that scope.
|
|
221
|
+
|
|
222
|
+
States:
|
|
223
|
+
- pending: scope is still running
|
|
224
|
+
- result ``None``: scope completed normally
|
|
225
|
+
- cancelled: scope ended through cancellation / scope-break control flow
|
|
226
|
+
- exception: scope failed
|
|
227
|
+
|
|
228
|
+
Calling ``scope.finished.cancel()`` while the scope is still running
|
|
229
|
+
requests cancellation of that scope.
|
|
230
|
+
"""
|
|
231
|
+
if self._finished is None:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
"Scope.finished is available only after entering the scope"
|
|
234
|
+
)
|
|
235
|
+
return self._finished
|
|
236
|
+
|
|
237
|
+
def cancel(self) -> None:
|
|
238
|
+
"""Request early stop of this scope.
|
|
239
|
+
|
|
240
|
+
This is different from reaching the natural end of the block:
|
|
241
|
+
``cancel()`` injects an internal cancellation task into the scope's
|
|
242
|
+
TaskGroup so the scope will tear down on exit.
|
|
243
|
+
|
|
244
|
+
This is useful when you need to stop a scope that is not the current
|
|
245
|
+
one, or when teardown request and control-flow exit are separate
|
|
246
|
+
concerns.
|
|
247
|
+
|
|
248
|
+
To exit the current scope immediately and continue after the block, use
|
|
249
|
+
``raise afor.ScopeBreak()`` instead.
|
|
250
|
+
|
|
251
|
+
Multiple calls are harmless.
|
|
252
|
+
"""
|
|
253
|
+
if self._cancel_called:
|
|
254
|
+
return
|
|
255
|
+
logger.debug("Cancelling scope")
|
|
256
|
+
self._cancel_called = True
|
|
257
|
+
assert self._task_group is not None
|
|
258
|
+
self._cancel_tg()
|
|
259
|
+
|
|
260
|
+
def _cancel_tg(self) -> None:
|
|
261
|
+
if self._task_group is None:
|
|
262
|
+
return
|
|
263
|
+
logger.debug("Cancelling task_group")
|
|
264
|
+
for t in self._task_group._tasks:
|
|
265
|
+
t.cancel()
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def _strip_scope_cancel(cls, err: BaseException) -> BaseException | None:
|
|
269
|
+
if isinstance(err, _ScopeCancelled):
|
|
270
|
+
return None
|
|
271
|
+
if isinstance(err, BaseExceptionGroup):
|
|
272
|
+
remaining = []
|
|
273
|
+
for inner in err.exceptions:
|
|
274
|
+
stripped = cls._strip_scope_cancel(inner)
|
|
275
|
+
if stripped is not None:
|
|
276
|
+
remaining.append(stripped)
|
|
277
|
+
if not remaining:
|
|
278
|
+
return None
|
|
279
|
+
return err.derive(remaining)
|
|
280
|
+
return err
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def _strip_scope_break(cls, err: BaseException) -> BaseException | None:
|
|
284
|
+
if isinstance(err, ScopeBreak):
|
|
285
|
+
return None
|
|
286
|
+
if isinstance(err, BaseExceptionGroup):
|
|
287
|
+
remaining = []
|
|
288
|
+
for inner in err.exceptions:
|
|
289
|
+
stripped = cls._strip_scope_break(inner)
|
|
290
|
+
if stripped is not None:
|
|
291
|
+
remaining.append(stripped)
|
|
292
|
+
if not remaining:
|
|
293
|
+
return None
|
|
294
|
+
return err.derive(remaining)
|
|
295
|
+
return err
|
|
296
|
+
|
|
297
|
+
def _set_finished_result(self) -> None:
|
|
298
|
+
if self._finished is None:
|
|
299
|
+
return
|
|
300
|
+
if not self._finished.done():
|
|
301
|
+
self._updating_finished = True
|
|
302
|
+
try:
|
|
303
|
+
self._finished.set_result(None)
|
|
304
|
+
finally:
|
|
305
|
+
self._updating_finished = False
|
|
306
|
+
|
|
307
|
+
def _set_finished_cancelled(self) -> None:
|
|
308
|
+
if self._finished is None:
|
|
309
|
+
return
|
|
310
|
+
if not self._finished.done():
|
|
311
|
+
self._updating_finished = True
|
|
312
|
+
try:
|
|
313
|
+
self._finished.cancel()
|
|
314
|
+
finally:
|
|
315
|
+
self._updating_finished = False
|
|
316
|
+
|
|
317
|
+
def _set_finished_exception(self, exc: BaseException) -> None:
|
|
318
|
+
if self._finished is None:
|
|
319
|
+
return
|
|
320
|
+
if not self._finished.done():
|
|
321
|
+
self._updating_finished = True
|
|
322
|
+
try:
|
|
323
|
+
self._finished.set_exception(exc)
|
|
324
|
+
finally:
|
|
325
|
+
self._updating_finished = False
|
|
326
|
+
|
|
327
|
+
def _finished_done_callback(self, fut: asyncio.Future[None]) -> None:
|
|
328
|
+
if self._updating_finished:
|
|
329
|
+
return
|
|
330
|
+
if not fut.cancelled():
|
|
331
|
+
return
|
|
332
|
+
if self._task_group is None:
|
|
333
|
+
return
|
|
334
|
+
self.cancel()
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _finished_consume_exception_callback(fut: asyncio.Future[None]) -> None:
|
|
338
|
+
if fut.cancelled():
|
|
339
|
+
return
|
|
340
|
+
try:
|
|
341
|
+
fut.exception()
|
|
342
|
+
except BaseException:
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# func: Callable[_P, Coroutine[_A, _Y, _R]],
|
|
347
|
+
# ) -> Callable[_P, Coroutine[_A, _Y, _R]]:
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def scoped(
|
|
351
|
+
func: Callable[_P, Coroutine[Any, Any, _R]],
|
|
352
|
+
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
353
|
+
"""Decorator: wrap an async function in ``async with afor.Scope():``.
|
|
354
|
+
|
|
355
|
+
Resources created inside the function auto-attach to the scope and are
|
|
356
|
+
cleaned up when the function returns (or raises). Equivalent to::
|
|
357
|
+
|
|
358
|
+
async def main():
|
|
359
|
+
async with afor.Scope():
|
|
360
|
+
... # your code
|
|
361
|
+
|
|
362
|
+
but with one less indentation level::
|
|
363
|
+
|
|
364
|
+
@afor.scoped
|
|
365
|
+
async def main():
|
|
366
|
+
... # your code
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
TypeError: If *func* is not an async function.
|
|
370
|
+
"""
|
|
371
|
+
if not inspect.iscoroutinefunction(func):
|
|
372
|
+
raise TypeError("@afor.scoped requires an async function")
|
|
373
|
+
|
|
374
|
+
@wraps(func)
|
|
375
|
+
async def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
376
|
+
async with Scope():
|
|
377
|
+
return await func(*args, **kwargs)
|
|
378
|
+
|
|
379
|
+
return wrapped
|