without-async 0.0.7__tar.gz → 0.0.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: without-async
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: The asyncio primitives without is built from: scoped background tasks, bounded concurrency, optional timeouts, and durations that cross an integer boundary.
5
5
  Author: Josh Karpel
6
6
  Author-email: Josh Karpel <josh.karpel@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "without-async"
7
- version = "0.0.7"
7
+ version = "0.0.9"
8
8
  description = "The asyncio primitives without is built from: scoped background tasks, bounded concurrency, optional timeouts, and durations that cross an integer boundary."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "without-async"
7
- version = "0.0.7"
7
+ version = "0.0.9"
8
8
  description = "The asyncio primitives without is built from: scoped background tasks, bounded concurrency, optional timeouts, and durations that cross an integer boundary."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,6 +4,7 @@ from without_async.tasks import as_async_iterator
4
4
  from without_async.tasks import background_task
5
5
  from without_async.tasks import cancel_futures
6
6
  from without_async.tasks import limit_concurrency
7
+ from without_async.tasks import settled
7
8
  from without_async.tasks import sleep_forever
8
9
  from without_async.tasks import timeout
9
10
 
@@ -14,6 +15,7 @@ __all__ = [
14
15
  "background_task",
15
16
  "cancel_futures",
16
17
  "limit_concurrency",
18
+ "settled",
17
19
  "sleep_forever",
18
20
  "timeout",
19
21
  ]
@@ -40,6 +40,33 @@ async def sleep_forever() -> None:
40
40
  await asyncio.get_running_loop().create_future()
41
41
 
42
42
 
43
+ async def settled[T](future: asyncio.Future[T]) -> T:
44
+ """
45
+ Await `future`, and hold on through a cancellation of the caller until it is done.
46
+
47
+ For a future whose work has already happened by the time it is awaited (a store write
48
+ recording an effect that has been performed, a renewal a server has already applied),
49
+ where cancelling the await would not undo the work but would lose the record of it.
50
+ The future is shielded, so the caller's cancellation does not reach it, and when one
51
+ arrives the caller waits for the future to finish before the `CancelledError`
52
+ propagates. What the future came to is then there to read off it, on the cancellation
53
+ path as on the ordinary one.
54
+
55
+ It waits through *repeated* cancellation, which is the ordinary case rather than
56
+ stubbornness: a task torn down by a task group or a `gather` is commonly cancelled
57
+ twice, once by the combinator and once by its caller's own teardown. `wait` rather than
58
+ an `await` for that, so the future's own failure belongs to whoever reads it afterwards
59
+ and does not replace the cancellation.
60
+ """
61
+ try:
62
+ return await asyncio.shield(future)
63
+ except asyncio.CancelledError:
64
+ while not future.done():
65
+ with suppress(asyncio.CancelledError):
66
+ await asyncio.wait([future])
67
+ raise
68
+
69
+
43
70
  async def cancel_futures[T](futures: Iterable[asyncio.Future[T] | None]) -> None:
44
71
  """
45
72
  Cancel every future, then await them all so their teardown completes.
File without changes