def test_outcomes_unwrap_raises_trio_error_over_qt_error(): """Unwrapping an Outcomes prioritizes a Trio error over a Qt error.""" class LocalUniqueException(Exception): pass class AnotherUniqueException(Exception): pass this_outcome = qtrio.Outcomes( qt=outcome.Error(AnotherUniqueException()), trio=outcome.Error(LocalUniqueException()), ) with pytest.raises(LocalUniqueException): this_outcome.unwrap()
def callback(): # Protect against race-conditions with condition: # Notify the fuse thread/winfsp that the call has started condition.notify() # Check for cancellation in a thread-safe way if cancelled: queue.put_nowait(RuntimeError("Cancelled")) return # Wrapper for the asynchronous function # Keyboard interrupt is enabled when using `trio_token.run_sync_soon`, # So let's disable it as we're heading back to user code. @trio.lowlevel.disable_ki_protection async def unprotected_afn(): return await afn(*args) # System task target async def await_in_trio_thread_task(): queue.put_nowait(await outcome.acapture(unprotected_afn)) # Spawn system task try: trio.lowlevel.spawn_system_task(await_in_trio_thread_task, name=afn) # System nursery is closed except RuntimeError: queue.put_nowait(outcome.Error(trio.RunFinishedError("system nursery is closed")))
async def _run(self) -> None: """ Context manager for this instance. """ # TODO nothing sets this ... yet self._close = trio.Event() async with trio.open_nursery() as n: # fx is the FIFO, opened in read-write mode fx = await n.start(self._reader) self.main = n self._is_running.set() try: await self._is_connected.wait() if self._handlers: async with trio.open_nursery() as nn: for event in self._handlers.keys(): nn.start_soon(partial(self.rpc,action="handle", event=event)) self.do_register_aliases() yield self finally: if fx is not None: os.close(fx) for k,v in self._replies.items(): if isinstance(v,trio.Event): self._replies[k] = outcome.Error(EOFError()) v.set() n.cancel_scope.cancel() pass # end finally pass # end nursery
async def __trio_thread_main(self): # The non-context-manager equivalent of open_loop() async with trio.open_nursery() as nursery: asyncio.set_event_loop(self) await self._main_loop_init(nursery) self.__blocking_result_queue.put(True) while not self._closed: # This *blocks* req = self.__blocking_job_queue.get() if req is None: self.stop() break async_fn, args = req result = await outcome.acapture(async_fn, *args) if type(result) == outcome.Error and type( result.error) == trio.Cancelled: res = RuntimeError("Main loop cancelled") res.__cause__ = result.error.__cause__ result = outcome.Error(res) self.__blocking_result_queue.put(result) with trio.CancelScope(shield=True): await self._main_loop_exit() self.__blocking_result_queue.put(None) nursery.cancel_scope.cancel()
async def wrapper(asyncfn, *args): nonlocal result try: await asyncfn(*args) except Exception as e: result = outcome.Error(e) event.set()
def test_outcomes_unwrap_raises_qt_error_over_trio_none(): """Unwrapping an Outcomes prioritizes a Qt error over a Trio None.""" class LocalUniqueException(Exception): pass this_outcome = qtrio.Outcomes(qt=outcome.Error(LocalUniqueException())) with pytest.raises(LocalUniqueException): this_outcome.unwrap()
def notify_closing(self, handle): handle = _get_base_socket(handle) waiters = self._afd_waiters.get(handle) if waiters is not None: if waiters.read_task is not None: _core.reschedule( waiters.read_task, outcome.Error(_core.ClosedResourceError()) ) waiters.read_task = None if waiters.write_task is not None: _core.reschedule( waiters.write_task, outcome.Error(_core.ClosedResourceError()) ) waiters.write_task = None self._refresh_afd(handle)
def notify_socket_close(self, sock): if not isinstance(sock, int): sock = sock.fileno() for mode in ["read", "write"]: if sock in self._socket_waiters[mode]: task = self._socket_waiters[mode].pop(sock) exc = _core.ClosedResourceError( "another task closed this socket") _core.reschedule(task, outcome.Error(exc))
async def _second_runner(self, queue: RequestQueue[int, int]) -> None: while True: many = await queue.get_many() for val, coro in many[::-1]: try: result = await self.queue.request(val + 100) except Exception as e: coro.resume(outcome.Error(e)) else: coro.resume(outcome.Value(result))
def notify_socket_close(self, sock): if type(sock) is not stdlib_socket.socket: raise TypeError("need a stdlib socket") for mode in ["read", "write"]: if sock in self._socket_waiters[mode]: task = self._socket_waiters[mode].pop(sock) exc = _core.ClosedResourceError( "another task closed this socket") _core.reschedule(task, outcome.Error(exc))
async def _first_runner(self, queue: RequestQueue) -> None: while True: many = await queue.get_many() for val, coro in many[::-1]: if val == 1337: try: failing_function(x) # type: ignore except Exception as e: coro.resume(outcome.Error(e)) else: coro.resume(outcome.Value(val + 10))
def outcome_from_application_return_code(return_code: int) -> outcome.Outcome: """Create either an :class:`outcome.Value` in the case of a 0 `return_code` or an :class:`outcome.Error` with a :class:`ReturnCodeError` otherwise. Args: return_code: The return code to be processed. """ if return_code == 0: return outcome.Value(return_code) return outcome.Error(qtrio.ReturnCodeError(return_code))
async def consume_next(): try: item = await async_generator.__anext__() result = outcome.Value(value=item) except StopAsyncIteration: result = outcome.Value(value=STOP) except asyncio.CancelledError: # Once we are cancelled, we do not call reschedule() anymore return except Exception as e: result = outcome.Error(error=e) trio.hazmat.reschedule(task, result)
async def receiver(): async with child_recv_chan: async for i in child_recv_chan: # Just consume all results from the channel until exhausted pass # And then wrap up the result and push it to the parent channel errors = [e.error for e in result_list if isinstance(e, outcome.Error)] if len(errors) > 0: result = outcome.Error(trio.MultiError(errors)) else: result = outcome.Value([o.unwrap() for o in result_list]) async with parent_send_chan: await parent_send_chan.send(result)
def callback(q, afn, args): @disable_ki_protection async def unprotected_afn(): coro = coroutine_or_error(afn, *args) return await coro async def await_in_trio_thread_task(): q.put_nowait(await outcome.acapture(unprotected_afn)) try: trio.lowlevel.spawn_system_task(await_in_trio_thread_task, name=afn) except RuntimeError: # system nursery is closed q.put_nowait( outcome.Error( trio.RunFinishedError("system nursery is closed")))
def wake_all(waiters, exc): try: current_task = _core.current_task() except RuntimeError: current_task = None raise_at_end = False for attr_name in ["read_task", "write_task"]: task = getattr(waiters, attr_name) if task is not None: if task is current_task: raise_at_end = True else: _core.reschedule(task, outcome.Error(copy.copy(exc))) setattr(waiters, attr_name, None) if raise_at_end: raise exc
async def consume_next(): t = sniffio.current_async_library_cvar.set("asyncio") try: item = await async_generator.__anext__() result = outcome.Value(value=item) except StopAsyncIteration: result = outcome.Value(value=STOP) except asyncio.CancelledError: # Once we are cancelled, we do not call reschedule() anymore return except Exception as e: result = outcome.Error(error=e) finally: sniffio.current_async_library_cvar.reset(t) trio.hazmat.reschedule(task, result)
def notify_closing(self, fd): if not isinstance(fd, int): fd = fd.fileno() for filter in [select.KQ_FILTER_READ, select.KQ_FILTER_WRITE]: key = (fd, filter) receiver = self._registered.get(key) if receiver is None: continue if type(receiver) is _core.Task: event = select.kevent(fd, filter, select.KQ_EV_DELETE) self._kqueue.control([event], 0) exc = _core.ClosedResourceError("another task closed this fd") _core.reschedule(receiver, outcome.Error(exc)) del self._registered[key] else: # XX this is an interesting example of a case where being able # to close a queue would be useful... raise NotImplementedError( "can't close an fd that monitor_kevent is using")
async def _msg_in(self, msg): if msg.get("result",()) and msg["result"][0] != "Pong": self.__logger.debug("IN %r",msg) seq = msg.get("seq",None) if seq is not None: try: ev = self._replies[seq] except KeyError: self.__logger.warning("Unknown Reply %r",msg) return else: if not isinstance(ev, trio.Event): self.__logger.warning("Dup Reply %r",msg) return try: self._replies[seq] = outcome.Value(msg["result"]) except KeyError: self._replies[seq] = outcome.Error(RuntimeError(msg.get("error","Unknown error"))) ev.set() else: await self._dispatch(msg) self.__logger.debug("IN done")
def interceptor(underlying_coro): # type: ignore nonlocal result, delivered try: next_trap = "hello" while True: trap_result = yield next_trap if isinstance(trap_result, outcome.Error): if isinstance(trap_result.error, MonitorKill): delivered = True next_trap = underlying_coro.send(trap_result) if not delivered: next_trap = underlying_coro.send( outcome.Error(MonitorKill())) delivered = True except StopIteration as ex: return ex.value except MonitorKill as ex: result = ex except BaseException as ex: result = ex raise finally: done.set()
async def set_error(self, exc): """Set the internal flag value to True, and wake any waiting tasks.""" self.value = outcome.Error(exc) await self.event.set()
def interrupt(task): exc = _core.ClosedResourceError("another task closed this fd") _core.reschedule(task, outcome.Error(exc))
async def set_error(self, err): await self._q_w.send(outcome.Error(err))
def _greenback_shim(orig_coro: Coroutine[Any, Any, Any]) -> Generator[Any, Any, Any]: # In theory this could be written as a simpler function that uses # _greenback_shim_sync(): # # next_yield = "ready" # while True: # try: # target = partial(orig_coro.send, (yield next_yield)) # except BaseException as ex: # target = partial(orig_coro.throw, ex) # try: # next_yield = yield from _greenback_shim_sync(target) # except StopIteration as ex: # return ex.value # # In practice, this doesn't work that well: _greenback_shim_sync() # has a hard time raising StopIteration, because it's a generator, # and unrolling it into a non-generator iterable makes it slower. # So we'll accept a bit of code duplication. parent_greenlet = greenlet.getcurrent() # The greenlet in which each send() or throw() call will occur. child_greenlet: Optional[greenlet.greenlet] = None # The contextvars.Context that we have most recently seen as active # for this task and propagated to child_greenlet curr_ctx: Optional[contextvars.Context] = None # The next thing we plan to yield to the event loop. (The first yield # goes to ensure_portal() rather than to the event loop, so we use a # string that is unlikely to be a valid event loop trap.) next_yield: Any = "ready" # The next thing we plan to send to the original coroutine. This is an # outcome representing the value or error that the event loop resumed # us with. next_send: outcome.Outcome[Any] while True: try: # Normally we send to orig_coro whatever the event loop sent us next_send = outcome.Value((yield next_yield)) except BaseException as ex: # If the event loop resumed us with an error, we forward that error next_send = outcome.Error(ex) try: if not child_greenlet: # Start a new send() or throw() call on the original coroutine. child_greenlet = greenlet.greenlet(next_send.send) switch_arg: Any = orig_coro else: # Resume the previous send() or throw() call, which is currently # at a simulated yield point in a greenback.await_() call. switch_arg = next_send if ( greenlet_needs_context_fixup and parent_greenlet.gr_context is not curr_ctx and child_greenlet.gr_context is curr_ctx ): # Make sure the child greenlet's contextvars context # is the same as our own, even if our own context # changes (such as via trio.Task.context assignment), # unless the child greenlet appears to have changed # its context privately through a call to Context.run(). # # Note 'parent_greenlet.gr_context' here is just a # portable way of getting the current contextvars # context, which is not exposed by the contextvars # module directly (copy_context() returns a copy, not # a new reference to the original). Upon initial # creation of child_greenlet, curr_ctx and # child_greenlet.gr_context will both be None, so this # condition works for that case too. child_greenlet.gr_context = curr_ctx = parent_greenlet.gr_context next_yield = child_greenlet.switch(switch_arg) if child_greenlet.dead: # The send() or throw() call completed so we need to # create a new greenlet for the next one. child_greenlet = curr_ctx = None except StopIteration as ex: # The underlying coroutine completed, so we forward its return value. return ex.value
def _greenback_shim_sync(target: Callable[[], Any]) -> Generator[Any, Any, Any]: """Run target(), forwarding the event loop traps and responses necessary to implement any await_() calls that it makes. This is only a little bit faster than using greenback_shim() plus a sync-to-async wrapper -- maybe 2us faster for the entire call, so it only matters when you're scoping the portal to a very small range. We ship it anyway because it's easier to understand than the async-compatible _greenback_shim(), and helps with understanding the latter. """ parent_greenlet = greenlet.getcurrent() curr_ctx = None # The greenlet in which we run target(). child_greenlet = greenlet.greenlet(target) # The next thing we plan to yield to the event loop. next_yield: Any # The next thing we plan to send via greenlet.switch(). This is an # outcome representing the value or error that the event loop resumed # us with. Initially None for the very first zero-argument switch(). next_send: Optional[outcome.Outcome[Any]] = None while True: if ( greenlet_needs_context_fixup and parent_greenlet.gr_context is not curr_ctx and child_greenlet.gr_context is curr_ctx ): # Make sure the child greenlet's contextvars context # is the same as our own, even if our own context # changes (such as via trio.Task.context assignment), # unless the child greenlet appears to have changed # its context privately through a call to Context.run(). # # Note 'parent_greenlet.gr_context' here is just a # portable way of getting the current contextvars # context, which is not exposed by the contextvars # module directly (copy_context() returns a copy, not # a new reference to the original). Upon initial # creation of child_greenlet, curr_ctx and # child_greenlet.gr_context will both be None, so this # condition works for that case too. child_greenlet.gr_context = curr_ctx = parent_greenlet.gr_context if next_send is None: next_yield = child_greenlet.switch() else: next_yield = child_greenlet.switch(next_send) if child_greenlet.dead: # target() returned, so next_yield is its return value, not an # event loop trap. (If it exits with an exception, that exception # will propagate out of switch() and thus out of the loop, which # is what we want.) return next_yield try: # Normally we send to orig_coro whatever the event loop sent us next_send = outcome.Value((yield next_yield)) except BaseException as ex: # If the event loop resumed us with an error, we forward that error next_send = outcome.Error(ex)
def test_outcome_from_application_return_code_error(): """Non-zero return code result in outcome.Error""" result = qtrio._core.outcome_from_application_return_code(return_code=-1) assert result == outcome.Error(qtrio.ReturnCodeError(-1))
def throw(self, exn: BaseException) -> None: return self.resume(outcome.Error(exn))
async def set_error(self, exc): """Set the result to raise this exceptio, and wake any waiting task. """ self.value = outcome.Error(exc) await self.event.set()
def socket_check(what, sock): try: select([sock], [sock], [sock], 0) except OSError as exc: socket_ready(what, sock, outcome.Error(exc))
def resume(self, value: Outcome[SendType]) -> None: if self.cancelled: # discard the result - not great, obviously... logger.debug("TrioContinuation(%s): resumed after cancellation", self.task) return if self.on_stack: logger.debug("TrioContinuation(%s): immediately resumed with %s", self.task, value) # This will happen if the function passed to shift immediately resumes the # continuation. With trio, we run the function passed to shift on the # coroutine that's being suspended. So we can't resume the coroutine here, # since it's already running. Instead we'll save the outcome, and in shift() # we check saved_send and just return immediately if it's set. This is not # normal shift/reset semantics but it's the best we can do with how trio is # structured. self.saved_send = value return resuming_task = GLOBAL_RUN_CONTEXT.task runner = GLOBAL_RUN_CONTEXT.runner logger.debug("TrioContinuation(%s): resuming with %s", self.task, value) global _under_coro_runner try: previous_runner = _under_coro_runner _under_coro_runner = Runner.TRIO # We have to temporarily set GLOBAL_RUN_CONTEXT.task to the task that is being # resumed; after all, that's the task that's really going to be running. This # wouldn't be necessary if we had proper dynamically scoped variables in # Python :( GLOBAL_RUN_CONTEXT.task = self.task # a little bit of reschedule(), before we run the task self.task._abort_func = None self.task.custom_sleep_data = None try: msg = self.task.context.run(self.task.coro.send, value) except StopIteration as exn: logger.debug("TrioContinuation(%s): return %s", self.task, exn.value) GLOBAL_RUN_CONTEXT.runner.task_exited(self.task, outcome.Value(exn.value)) return except BaseException as exn: logger.debug("TrioContinuation(%s): raised %s", self.task, exn) exn = exn.with_traceback(exn.__traceback__ and exn.__traceback__.tb_next) GLOBAL_RUN_CONTEXT.runner.task_exited(self.task, outcome.Error(exn)) return logger.debug("TrioContinuation(%s): yield %s", self.task, msg) finally: _under_coro_runner = previous_runner GLOBAL_RUN_CONTEXT.task = resuming_task if msg is CancelShieldedCheckpoint: runner.reschedule(self.task) elif type(msg) is WaitTaskRescheduled: self.task._abort_func = msg.abort_func if runner.ki_pending and self.task is runner.main_task: self.task._attempt_delivery_of_pending_ki() self.task._attempt_delivery_of_any_pending_cancel() elif type(msg) is PermanentlyDetachCoroutineObject: runner.task_exited(self.task, msg.final_outcome) else: raise TypeError("bad yield from continuation", msg)