Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
taskgroups.py281 linesDownload Raw Back to asyncio
1# Adapted with permission from the EdgeDB project;2# license: PSFL.3 4 5__all__ = ("TaskGroup",)6 7from . import events8from . import exceptions9from . import futures10from . import tasks11 12 13class TaskGroup:14    """Asynchronous context manager for managing groups of tasks.15 16    Example use:17 18        async with asyncio.TaskGroup() as group:19            task1 = group.create_task(some_coroutine(...))20            task2 = group.create_task(other_coroutine(...))21        print("Both tasks have completed now.")22 23    All tasks are awaited when the context manager exits.24 25    Any exceptions other than `asyncio.CancelledError` raised within26    a task will cancel all remaining tasks and wait for them to exit.27    The exceptions are then combined and raised as an `ExceptionGroup`.28    """29    def __init__(self):30        self._entered = False31        self._exiting = False32        self._aborting = False33        self._loop = None34        self._parent_task = None35        self._parent_cancel_requested = False36        self._tasks = set()37        self._errors = []38        self._base_error = None39        self._on_completed_fut = None40 41    def __repr__(self):42        info = ['']43        if self._tasks:44            info.append(f'tasks={len(self._tasks)}')45        if self._errors:46            info.append(f'errors={len(self._errors)}')47        if self._aborting:48            info.append('cancelling')49        elif self._entered:50            info.append('entered')51 52        info_str = ' '.join(info)53        return f'<TaskGroup{info_str}>'54 55    async def __aenter__(self):56        if self._entered:57            raise RuntimeError(58                f"TaskGroup {self!r} has already been entered")59        if self._loop is None:60            self._loop = events.get_running_loop()61        self._parent_task = tasks.current_task(self._loop)62        if self._parent_task is None:63            raise RuntimeError(64                f'TaskGroup {self!r} cannot determine the parent task')65        self._entered = True66 67        return self68 69    async def __aexit__(self, et, exc, tb):70        tb = None71        try:72            return await self._aexit(et, exc)73        finally:74            # Exceptions are heavy objects that can have object75            # cycles (bad for GC); let's not keep a reference to76            # a bunch of them. It would be nicer to use a try/finally77            # in __aexit__ directly but that introduced some diff noise78            self._parent_task = None79            self._errors = None80            self._base_error = None81            exc = None82 83    async def _aexit(self, et, exc):84        self._exiting = True85 86        if (exc is not None and87                self._is_base_error(exc) and88                self._base_error is None):89            self._base_error = exc90 91        if et is not None and issubclass(et, exceptions.CancelledError):92            propagate_cancellation_error = exc93        else:94            propagate_cancellation_error = None95 96        if et is not None:97            if not self._aborting:98                # Our parent task is being cancelled:99                #100                #    async with TaskGroup() as g:101                #        g.create_task(...)102                #        await ...  # <- CancelledError103                #104                # or there's an exception in "async with":105                #106                #    async with TaskGroup() as g:107                #        g.create_task(...)108                #        1 / 0109                #110                self._abort()111 112        # We use while-loop here because "self._on_completed_fut"113        # can be cancelled multiple times if our parent task114        # is being cancelled repeatedly (or even once, when115        # our own cancellation is already in progress)116        while self._tasks:117            if self._on_completed_fut is None:118                self._on_completed_fut = self._loop.create_future()119 120            try:121                await self._on_completed_fut122            except exceptions.CancelledError as ex:123                if not self._aborting:124                    # Our parent task is being cancelled:125                    #126                    #    async def wrapper():127                    #        async with TaskGroup() as g:128                    #            g.create_task(foo)129                    #130                    # "wrapper" is being cancelled while "foo" is131                    # still running.132                    propagate_cancellation_error = ex133                    self._abort()134 135            self._on_completed_fut = None136 137        assert not self._tasks138 139        if self._base_error is not None:140            try:141                raise self._base_error142            finally:143                exc = None144 145        if self._parent_cancel_requested:146            # If this flag is set we *must* call uncancel().147            if self._parent_task.uncancel() == 0:148                # If there are no pending cancellations left,149                # don't propagate CancelledError.150                propagate_cancellation_error = None151 152        # Propagate CancelledError if there is one, except if there153        # are other errors -- those have priority.154        try:155            if propagate_cancellation_error is not None and not self._errors:156                try:157                    raise propagate_cancellation_error158                finally:159                    exc = None160        finally:161            propagate_cancellation_error = None162 163        if et is not None and not issubclass(et, exceptions.CancelledError):164            self._errors.append(exc)165 166        if self._errors:167            # If the parent task is being cancelled from the outside168            # of the taskgroup, un-cancel and re-cancel the parent task,169            # which will keep the cancel count stable.170            if self._parent_task.cancelling():171                self._parent_task.uncancel()172                self._parent_task.cancel()173            try:174                raise BaseExceptionGroup(175                    'unhandled errors in a TaskGroup',176                    self._errors,177                ) from None178            finally:179                exc = None180 181 182    def create_task(self, coro, **kwargs):183        """Create a new task in this group and return it.184 185        Similar to `asyncio.create_task`.186        """187        if not self._entered:188            coro.close()189            raise RuntimeError(f"TaskGroup {self!r} has not been entered")190        if self._exiting and not self._tasks:191            coro.close()192            raise RuntimeError(f"TaskGroup {self!r} is finished")193        if self._aborting:194            coro.close()195            raise RuntimeError(f"TaskGroup {self!r} is shutting down")196        task = self._loop.create_task(coro, **kwargs)197 198        futures.future_add_to_awaited_by(task, self._parent_task)199 200        # Always schedule the done callback even if the task is201        # already done (e.g. if the coro was able to complete eagerly),202        # otherwise if the task completes with an exception then it will cancel203        # the current task too early. gh-128550, gh-128588204        self._tasks.add(task)205        task.add_done_callback(self._on_task_done)206        try:207            return task208        finally:209            # gh-128552: prevent a refcycle of210            # task.exception().__traceback__->TaskGroup.create_task->task211            del task212 213    # Since Python 3.8 Tasks propagate all exceptions correctly,214    # except for KeyboardInterrupt and SystemExit which are215    # still considered special.216 217    def _is_base_error(self, exc: BaseException) -> bool:218        assert isinstance(exc, BaseException)219        return isinstance(exc, (SystemExit, KeyboardInterrupt))220 221    def _abort(self):222        self._aborting = True223 224        for t in self._tasks:225            if not t.done():226                t.cancel()227 228    def _on_task_done(self, task):229        self._tasks.discard(task)230 231        futures.future_discard_from_awaited_by(task, self._parent_task)232 233        if self._on_completed_fut is not None and not self._tasks:234            if not self._on_completed_fut.done():235                self._on_completed_fut.set_result(True)236 237        if task.cancelled():238            return239 240        exc = task.exception()241        if exc is None:242            return243 244        self._errors.append(exc)245        if self._is_base_error(exc) and self._base_error is None:246            self._base_error = exc247 248        if self._parent_task.done():249            # Not sure if this case is possible, but we want to handle250            # it anyways.251            self._loop.call_exception_handler({252                'message': f'Task {task!r} has errored out but its parent '253                           f'task {self._parent_task} is already completed',254                'exception': exc,255                'task': task,256            })257            return258 259        if not self._aborting and not self._parent_cancel_requested:260            # If parent task *is not* being cancelled, it means that we want261            # to manually cancel it to abort whatever is being run right now262            # in the TaskGroup.  But we want to mark parent task as263            # "not cancelled" later in __aexit__.  Example situation that264            # we need to handle:265            #266            #    async def foo():267            #        try:268            #            async with TaskGroup() as g:269            #                g.create_task(crash_soon())270            #                await something  # <- this needs to be canceled271            #                                 #    by the TaskGroup, e.g.272            #                                 #    foo() needs to be cancelled273            #        except Exception:274            #            # Ignore any exceptions raised in the TaskGroup275            #            pass276            #        await something_else     # this line has to be called277            #                                 # after TaskGroup is finished.278            self._abort()279            self._parent_cancel_requested = True280            self._parent_task.cancel()281 
codekingpro/portable-devtools · Team Ai