|
22 | 22 | Callable,
|
23 | 23 | Dict,
|
24 | 24 | Generic,
|
| 25 | + Iterable, |
25 | 26 | Iterator,
|
26 | 27 | List,
|
27 | 28 | Mapping,
|
|
31 | 32 | Sequence,
|
32 | 33 | Tuple,
|
33 | 34 | Type,
|
| 35 | + TypeVar, |
34 | 36 | Union,
|
35 | 37 | cast,
|
36 | 38 | overload,
|
@@ -4361,6 +4363,176 @@ def set_dynamic_update_handler(
|
4361 | 4363 | _Runtime.current().workflow_set_update_handler(None, handler, validator)
|
4362 | 4364 |
|
4363 | 4365 |
|
| 4366 | +def as_completed( |
| 4367 | + fs: Iterable[Awaitable[AnyType]], *, timeout: Optional[float] = None |
| 4368 | +) -> Iterator[Awaitable[AnyType]]: |
| 4369 | + """Return an iterator whose values are coroutines. |
| 4370 | +
|
| 4371 | + This is a deterministic version of :py:func:`asyncio.as_completed`. This |
| 4372 | + function should be used instead of that one in workflows. |
| 4373 | + """ |
| 4374 | + # Taken almost verbatim from |
| 4375 | + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L584 |
| 4376 | + # but the "set" is changed out for a "list" and fixed up some typing/format |
| 4377 | + |
| 4378 | + if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): |
| 4379 | + raise TypeError(f"expect an iterable of futures, not {type(fs).__name__}") |
| 4380 | + |
| 4381 | + done = asyncio.Queue[Optional[asyncio.Future]]() |
| 4382 | + |
| 4383 | + loop = asyncio.get_event_loop() |
| 4384 | + todo: List[asyncio.Future] = [asyncio.ensure_future(f, loop=loop) for f in list(fs)] |
| 4385 | + timeout_handle = None |
| 4386 | + |
| 4387 | + def _on_timeout(): |
| 4388 | + for f in todo: |
| 4389 | + f.remove_done_callback(_on_completion) |
| 4390 | + done.put_nowait(None) # Queue a dummy value for _wait_for_one(). |
| 4391 | + todo.clear() # Can't do todo.remove(f) in the loop. |
| 4392 | + |
| 4393 | + def _on_completion(f): |
| 4394 | + if not todo: |
| 4395 | + return # _on_timeout() was here first. |
| 4396 | + todo.remove(f) |
| 4397 | + done.put_nowait(f) |
| 4398 | + if not todo and timeout_handle is not None: |
| 4399 | + timeout_handle.cancel() |
| 4400 | + |
| 4401 | + async def _wait_for_one(): |
| 4402 | + f = await done.get() |
| 4403 | + if f is None: |
| 4404 | + # Dummy value from _on_timeout(). |
| 4405 | + raise asyncio.TimeoutError |
| 4406 | + return f.result() # May raise f.exception(). |
| 4407 | + |
| 4408 | + for f in todo: |
| 4409 | + f.add_done_callback(_on_completion) |
| 4410 | + if todo and timeout is not None: |
| 4411 | + timeout_handle = loop.call_later(timeout, _on_timeout) |
| 4412 | + for _ in range(len(todo)): |
| 4413 | + yield _wait_for_one() |
| 4414 | + |
| 4415 | + |
| 4416 | +if TYPE_CHECKING: |
| 4417 | + _FT = TypeVar("_FT", bound=asyncio.Future[Any]) |
| 4418 | +else: |
| 4419 | + _FT = TypeVar("_FT", bound=asyncio.Future) |
| 4420 | + |
| 4421 | + |
| 4422 | +@overload |
| 4423 | +async def wait( # type: ignore[misc] |
| 4424 | + fs: Iterable[_FT], |
| 4425 | + *, |
| 4426 | + timeout: Optional[float] = None, |
| 4427 | + return_when: str = asyncio.ALL_COMPLETED, |
| 4428 | +) -> Tuple[List[_FT], List[_FT]]: |
| 4429 | + ... |
| 4430 | + |
| 4431 | + |
| 4432 | +@overload |
| 4433 | +async def wait( |
| 4434 | + fs: Iterable[asyncio.Task[AnyType]], |
| 4435 | + *, |
| 4436 | + timeout: Optional[float] = None, |
| 4437 | + return_when: str = asyncio.ALL_COMPLETED, |
| 4438 | +) -> Tuple[List[asyncio.Task[AnyType]], set[asyncio.Task[AnyType]]]: |
| 4439 | + ... |
| 4440 | + |
| 4441 | + |
| 4442 | +async def wait( |
| 4443 | + fs: Iterable, |
| 4444 | + *, |
| 4445 | + timeout: Optional[float] = None, |
| 4446 | + return_when: str = asyncio.ALL_COMPLETED, |
| 4447 | +) -> Tuple: |
| 4448 | + """Wait for the Futures or Tasks given by fs to complete. |
| 4449 | +
|
| 4450 | + This is a deterministic version of :py:func:`asyncio.wait`. This function |
| 4451 | + should be used instead of that one in workflows. |
| 4452 | + """ |
| 4453 | + # Taken almost verbatim from |
| 4454 | + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L435 |
| 4455 | + # but the "set" is changed out for a "list" and fixed up some typing/format |
| 4456 | + |
| 4457 | + if asyncio.isfuture(fs) or asyncio.iscoroutine(fs): |
| 4458 | + raise TypeError(f"expect a list of futures, not {type(fs).__name__}") |
| 4459 | + if not fs: |
| 4460 | + raise ValueError("Set of Tasks/Futures is empty.") |
| 4461 | + if return_when not in ( |
| 4462 | + asyncio.FIRST_COMPLETED, |
| 4463 | + asyncio.FIRST_EXCEPTION, |
| 4464 | + asyncio.ALL_COMPLETED, |
| 4465 | + ): |
| 4466 | + raise ValueError(f"Invalid return_when value: {return_when}") |
| 4467 | + |
| 4468 | + fs = list(fs) |
| 4469 | + |
| 4470 | + if any(asyncio.iscoroutine(f) for f in fs): |
| 4471 | + raise TypeError("Passing coroutines is forbidden, use tasks explicitly.") |
| 4472 | + |
| 4473 | + loop = asyncio.get_running_loop() |
| 4474 | + return await _wait(fs, timeout, return_when, loop) |
| 4475 | + |
| 4476 | + |
| 4477 | +async def _wait( |
| 4478 | + fs: Iterable[Union[asyncio.Future, asyncio.Task]], |
| 4479 | + timeout: Optional[float], |
| 4480 | + return_when: str, |
| 4481 | + loop: asyncio.AbstractEventLoop, |
| 4482 | +) -> Tuple[List, List]: |
| 4483 | + # Taken almost verbatim from |
| 4484 | + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L522 |
| 4485 | + # but the "set" is changed out for a "list" and fixed up some typing/format |
| 4486 | + |
| 4487 | + assert fs, "Set of Futures is empty." |
| 4488 | + waiter = loop.create_future() |
| 4489 | + timeout_handle = None |
| 4490 | + if timeout is not None: |
| 4491 | + timeout_handle = loop.call_later(timeout, _release_waiter, waiter) |
| 4492 | + counter = len(fs) # type: ignore[arg-type] |
| 4493 | + |
| 4494 | + def _on_completion(f): |
| 4495 | + nonlocal counter |
| 4496 | + counter -= 1 |
| 4497 | + if ( |
| 4498 | + counter <= 0 |
| 4499 | + or return_when == asyncio.FIRST_COMPLETED |
| 4500 | + or return_when == asyncio.FIRST_EXCEPTION |
| 4501 | + and (not f.cancelled() and f.exception() is not None) |
| 4502 | + ): |
| 4503 | + if timeout_handle is not None: |
| 4504 | + timeout_handle.cancel() |
| 4505 | + if not waiter.done(): |
| 4506 | + waiter.set_result(None) |
| 4507 | + |
| 4508 | + for f in fs: |
| 4509 | + f.add_done_callback(_on_completion) |
| 4510 | + |
| 4511 | + try: |
| 4512 | + await waiter |
| 4513 | + finally: |
| 4514 | + if timeout_handle is not None: |
| 4515 | + timeout_handle.cancel() |
| 4516 | + for f in fs: |
| 4517 | + f.remove_done_callback(_on_completion) |
| 4518 | + |
| 4519 | + done, pending = [], [] |
| 4520 | + for f in fs: |
| 4521 | + if f.done(): |
| 4522 | + done.append(f) |
| 4523 | + else: |
| 4524 | + pending.append(f) |
| 4525 | + return done, pending |
| 4526 | + |
| 4527 | + |
| 4528 | +def _release_waiter(waiter: asyncio.Future[Any], *args) -> None: |
| 4529 | + # Taken almost verbatim from |
| 4530 | + # https://github.com/python/cpython/blob/v3.12.3/Lib/asyncio/tasks.py#L467 |
| 4531 | + |
| 4532 | + if not waiter.done(): |
| 4533 | + waiter.set_result(None) |
| 4534 | + |
| 4535 | + |
4364 | 4536 | def _is_unbound_method_on_cls(fn: Callable[..., Any], cls: Type) -> bool:
|
4365 | 4537 | # Python 3 does not make this easy, ref https://stackoverflow.com/questions/3589311
|
4366 | 4538 | return (
|
|
0 commit comments