@@ -148,33 +148,54 @@ class chain(AsyncIterator[T]):
148148 The resulting iterator consecutively iterates over and yields all values from
149149 each of the ``iterables``. This is similar to converting all ``iterables`` to
150150 sequences and concatenating them, but lazily exhausts each iterable.
151+
152+ The ``chain`` assumes ownership of its ``iterables`` and closes them reliably
153+ when the ``chain`` is closed. Pass the ``iterables`` via a :py:class:`tuple` to
154+ ``chain.from_iterable`` to avoid closing all iterables but those already processed.
151155 """
152156
153- __slots__ = ("_impl" , )
157+ __slots__ = ("_iterator" , "_owned_iterators" )
154158
155- def __init__ (self , * iterables : AnyIterable [T ]):
156- async def impl () -> AsyncIterator [T ]:
157- for iterable in iterables :
159+ @staticmethod
160+ async def _chain_iterator (
161+ any_iterables : AnyIterable [AnyIterable [T ]],
162+ ) -> AsyncGenerator [T , None ]:
163+ async with ScopedIter (any_iterables ) as iterables :
164+ async for iterable in iterables :
158165 async with ScopedIter (iterable ) as iterator :
159166 async for item in iterator :
160167 yield item
161168
162- self ._impl = impl ()
169+ def __init__ (
170+ self , * iterables : AnyIterable [T ], _iterables : AnyIterable [AnyIterable [T ]] = ()
171+ ):
172+ self ._iterator = self ._chain_iterator (iterables or _iterables )
173+ self ._owned_iterators = (
174+ iterable
175+ for iterable in iterables
176+ if isinstance (iterable , AsyncIterator ) and hasattr (iterable , "aclose" )
177+ )
163178
164- @staticmethod
165- async def from_iterable (iterable : AnyIterable [AnyIterable [T ]]) -> AsyncIterator [T ]:
179+ @classmethod
180+ def from_iterable (cls , iterable : AnyIterable [AnyIterable [T ]]) -> "chain [T]" :
166181 """
167182 Alternate constructor for :py:func:`~.chain` that lazily exhausts
168- iterables as well
183+ the ``iterable`` of iterables as well
184+
185+ This is suitable for chaining iterables from a lazy or infinite ``iterable``.
186+ In turn, closing the ``chain`` only closes those iterables
187+ already fetched from ``iterable``.
169188 """
170- async with ScopedIter (iterable ) as iterables :
171- async for sub_iterable in iterables :
172- async with ScopedIter (sub_iterable ) as iterator :
173- async for item in iterator :
174- yield item
189+ return cls (_iterables = iterable )
175190
176191 def __anext__ (self ) -> Awaitable [T ]:
177- return self ._impl .__anext__ ()
192+ return self ._iterator .__anext__ ()
193+
194+ async def aclose (self ) -> None :
195+ for iterable in self ._owned_iterators :
196+ if hasattr (iterable , "aclose" ):
197+ await iterable .aclose ()
198+ await self ._iterator .aclose ()
178199
179200
180201async def compress (
0 commit comments