Skip to content

Commit 820a544

Browse files
Docs on iterator scoping (#71)
* separated howto/notes from library * hooked scoping guide into other pages
1 parent aca5581 commit 820a544

File tree

3 files changed

+107
-9
lines changed

3 files changed

+107
-9
lines changed

docs/index.rst

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ The missing ``async`` toolbox
4040
source/api/contextlib
4141
source/api/itertools
4242
source/api/asynctools
43+
44+
.. toctree::
45+
:maxdepth: 1
46+
:caption: Guides and Notes
47+
:hidden:
48+
49+
source/notes/iter_scope
4350
source/glossary
4451

4552
.. toctree::
@@ -128,18 +135,12 @@ Async Iterator Cleanup
128135
======================
129136

130137
Cleanup of async iterables is special in that :py:meth:`~agen.aclose` may require
131-
an active event loop. This is not given when garbage collection finalizes an
132-
async iterable via its :py:meth:`~object.__del__` method. Thus, async iterators
133-
should be cleaned up deterministically whenever possible (see `PEP 533`_ for details).
134-
135-
All async iterators of :py:mod:`asyncstdlib` that work on other iterators
136-
assume sole ownership of the iterators passed to them.
137-
Passed in async iterators are guaranteed to :py:meth:`~agen.aclose` as soon as
138-
the :py:mod:`asyncstdlib` async iterator itself is cleaned up.
138+
an active event loop. Thus, all utilities of :py:mod:`asyncstdlib` that work on async
139+
iterators will eagerly :py:meth:`~agen.aclose` them.
139140
Use :py:func:`~asyncstdlib.asynctools.borrow` to prevent automatic cleanup,
140141
and :py:func:`~asyncstdlib.asynctools.scoped_iter` to guarantee cleanup in custom code.
141142

142-
.. _PEP 533: https://www.python.org/dev/peps/pep-0533/
143+
See the guide on :ref:`guide_iteration` for details and usage examples.
143144

144145
Indices and tables
145146
==================

docs/source/api/itertools.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ The itertools library
88
The :py:mod:`asyncstdlib.itertools` library implements
99
Python's :py:mod:`itertools` for (async) functions and (async) iterables.
1010

11+
.. note::
12+
13+
To avoid leaking resources, all utilities in this module explicitly close their
14+
iterable arguments when done.
15+
This can be unexpected for non-exhausting utilities such as :py:func:`~.dropwhile`
16+
and may require explicit scoping.
17+
See the guide on :ref:`guide_iteration` for details and usage examples.
18+
1119
Infinite iterators
1220
==================
1321

docs/source/notes/iter_scope.rst

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
.. _guide_iteration:
2+
3+
================
4+
Iterator Scoping
5+
================
6+
7+
Cleanup of ``async`` resources is special in that it may require an active event loop.
8+
Since :term:`asynchronous iterators <python:asynchronous iterator>` can hold resources
9+
indefinitely, they should be cleaned up deterministically whenever possible
10+
(see `PEP 533`_ for discussion).
11+
Thus, ``asyncstdlib`` defaults to deterministic cleanup but provides tools to explicitly
12+
manage the lifetime of iterators.
13+
14+
Cleanup in ``asyncstdlib``
15+
==========================
16+
17+
All async iterators of :py:mod:`asyncstdlib` that work on other iterators
18+
assume sole ownership of the iterators passed to them.
19+
Passed in async iterators are guaranteed to be :py:meth:`~agen.aclose`\ d
20+
as soon as the :py:mod:`asyncstdlib` async iterator itself is cleaned up.
21+
This provides a resource-safe default for the most common operation of
22+
exhausting iterators.
23+
24+
.. code-block:: python3
25+
26+
>>> import asyncio
27+
>>> import asyncstdlib as a
28+
>>>
29+
>>> async def async_squares(i=0):
30+
... """Provide an infinite stream of squared numbers"""
31+
... while True:
32+
... await asyncio.sleep(0.1)
33+
... yield i**2
34+
... i += 1
35+
...
36+
>>> async def main():
37+
... async_iter = async_squares()
38+
... # loop until we are done
39+
... async for i, s in a.zip(range(5), async_iter):
40+
... print(f"{i}: {s}")
41+
... assert await a.anext(async_iter, "Closed!") == "Closed!"
42+
...
43+
>>> asyncio.run(main())
44+
45+
For consistency, every :py:mod:`asyncstdlib` async iterator performs such cleanup.
46+
This may be unexpected for async variants of iterator utilities that are usually
47+
applied multiple times, such as :py:func:`itertools.islice`.
48+
Thus, to manage the lifetime of async iterators one can explicitly scope them.
49+
50+
Scoping async iterator lifetime
51+
===============================
52+
53+
In order to use a single async iterator across several iterations but guarantee cleanup,
54+
the iterator can be scoped to an ``async with`` block:
55+
using :py:func:`asyncstdlib.scoped_iter` creates an async iterator that is guaranteed
56+
to :py:meth:`~agen.aclose` at the end of the block, but cannot be closed before.
57+
58+
.. code-block:: python3
59+
60+
>>> import asyncio
61+
>>> import asyncstdlib as a
62+
>>>
63+
>>> async def async_squares(i=0):
64+
... """Provide an infinite stream of squared numbers"""
65+
... while True:
66+
... await asyncio.sleep(0.1)
67+
... yield i**2
68+
... i += 1
69+
...
70+
>>> async def main():
71+
... # iterator can be re-used in the async with block
72+
... async with a.scoped_iter(async_squares()) as async_iter:
73+
... async for s in a.islice(async_iter, 3):
74+
... print(f"1st Batch: {s}")
75+
... # async_iter is still open for further iteration
76+
... async for s in a.islice(async_iter, 3):
77+
... print(f"2nd Batch: {s}")
78+
... async for s in a.islice(async_iter, 3):
79+
... print(f"3rd Batch: {s}")
80+
... # iterator is closed after the async with block
81+
... assert await a.anext(async_iter, "Closed!") == "Closed!"
82+
...
83+
>>> asyncio.run(main())
84+
85+
Scoped iterators should be the go-to approach for managing iterator lifetimes.
86+
However, not all lifetimes correspond to well-defined lexical scopes;
87+
for these cases, one can :term:`borrow <borrowing>` an iterator instead.
88+
89+
.. _PEP 533: https://www.python.org/dev/peps/pep-0533/

0 commit comments

Comments
 (0)