maxfischer2781 / asyncstdlib

the missing toolbox for an async world
https://asyncstdlib.readthedocs.io
MIT License
234 stars 21 forks source link

Change in behavior of chain() between 3.10.5 and 3.10.6. #107

Closed byllyfish closed 1 year ago

byllyfish commented 1 year ago

There is a subtle change in behavior introduced in the recent changes to chain(). Chain's iterator appears to be escaping from its "scope" when it is interrupted.

Here is an example program that produces different output. I'm using Python 3.11.2, but I don't think the Python version makes much difference.

import asyncio

import asyncstdlib as asl

async def gen1(*args):
    try:
        for i in args:
            yield i
    finally:
        print("FINALLY")

async def main():
    nums = asl.chain(gen1(1, 2, 3))
    nums = asl.takewhile(lambda x: x != 2, nums)

    async for n in nums:
        print(n)

    print("ALL DONE")

asyncio.run(main())

asyncstdlib 3.10.5 (correct output)

1
FINALLY
ALL DONE

asyncstdlib 3.10.6 (incorrect output)

1
ALL DONE
FINALLY
byllyfish commented 1 year ago

Here's a construction of chain that seems to work better:

class _Chain1:
    def __call__(self, *iterables):
        async def impl():
            for iterable in iterables:
                async with asl.scoped_iter(iterable) as iterator:
                    async for item in iterator:
                        yield item

        return impl()

    # Add other async methods here...

chain = _Chain1()
maxfischer2781 commented 1 year ago

It looks like this is due to a missing aclose (which a class implementation needs explicitly). Thanks for the report.

byllyfish commented 1 year ago

The fix you committed passes all my tests. Thanks!