diff --git a/micropython/ucontextlib-async/manifest.py b/micropython/ucontextlib-async/manifest.py new file mode 100644 index 00000000..bce9b2f3 --- /dev/null +++ b/micropython/ucontextlib-async/manifest.py @@ -0,0 +1,4 @@ +metadata(description="asynccontextmanager support in ucontextlib", version="0.1.1") + +require("ucontextlib") +package("ucontextlib") diff --git a/micropython/ucontextlib-async/tests.py b/micropython/ucontextlib-async/tests.py new file mode 100644 index 00000000..6799c018 --- /dev/null +++ b/micropython/ucontextlib-async/tests.py @@ -0,0 +1,43 @@ +import uasyncio +import unittest +from ucontextlib._async import asynccontextmanager + + +class AsyncContextManagerTestCase(unittest.TestCase): + def setUp(self): + self._history = [] + + @asynccontextmanager + async def manager(x): + self._history.append("start") + try: + await uasyncio.sleep_ms(0) + yield x + finally: + self._history.append("finish") + + self._manager = manager + + def test_context_manager(self): + async def _test(): + async with self._manager(123) as x: + self.assertEqual(x, 123) + self.assertEqual(self._history, ["start", "finish"]) + + uasyncio.run_until_complete(_test()) + + def test_context_manager_on_error(self): + async def _test(): + exc = Exception() + try: + async with self._manager(123) as x: + raise exc + except Exception as e: + self.assertEqual(exc, e) + self.assertEqual(self._history, ["start", "finish"]) + + uasyncio.run_until_complete(_test()) + + +if __name__ == "__main__": + unittest.main() diff --git a/micropython/ucontextlib-async/ucontextlib/_async.py b/micropython/ucontextlib-async/ucontextlib/_async.py new file mode 100644 index 00000000..7d88711f --- /dev/null +++ b/micropython/ucontextlib-async/ucontextlib/_async.py @@ -0,0 +1,222 @@ +"""Utilities for async with-statement contexts. + +Based on original source code: https://github.com/python/cpython/blob/3.11/Lib/contextlib.py + +""" + + +class ContextDecorator(object): + "A base class or mixin that enables context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. + """ + return self + + def __call__(self, func): + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + + return inner + + +class _GeneratorContextManager(ContextDecorator): + """Helper for @contextmanager decorator.""" + + def __init__(self, func, *args, **kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + + def _recreate_cm(self): + # _GCM instances are one-shot context managers, so the + # CM must be recreated each time a decorated function is + # called + return self.__class__(self.func, *self.args, **self.kwds) + + def __enter__(self): + try: + return next(self.gen) + except StopIteration: + raise RuntimeError("generator didn't yield") from None + + def __exit__(self, type, value, traceback): + if type is None: + try: + next(self.gen) + except StopIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = type() + try: + self.gen.throw(type, value, traceback) + raise RuntimeError("generator didn't stop after throw()") + except StopIteration as exc: + # Suppress the exception *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed + return exc is not value + + +def contextmanager(func): + """@contextmanager decorator. + + Typical usage: + + @contextmanager + def some_generator(): + + try: + yield + finally: + + + This makes this: + + with some_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + + """ + + def helper(*args, **kwds): + return _GeneratorContextManager(func, *args, **kwds) + + return helper + + +class AsyncContextDecorator(object): + "A base class or mixin that enables async context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self.""" + return self + + def __call__(self, func): + async def inner(*args, **kwds): + async with self._recreate_cm(): + return await func(*args, **kwds) + + return inner + + +class _AsyncGeneratorContextManager(AsyncContextDecorator): + """Helper for @asynccontextmanager decorator.""" + + def __init__(self, func, args, kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + + async def __aenter__(self): + # do not keep args and kwds alive unnecessarily + # they are only needed for recreation, which is not possible anymore + del self.args, self.kwds, self.func + try: + return await anext(self.gen) + except StopAsyncIteration: + raise RuntimeError("generator didn't yield") from None + + async def __aexit__(self, typ, value, traceback): + if typ is None: + try: + await anext(self.gen) + except StopAsyncIteration: + return False + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = typ() + try: + await self.gen.athrow(typ, value, traceback) + except StopAsyncIteration as exc: + # Suppress StopIteration *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed. + return exc is not value + except RuntimeError as exc: + # Don't re-raise the passed in exception. (issue27122) + if exc is value: + exc.__traceback__ = traceback + return False + # Avoid suppressing if a Stop(Async)Iteration exception + # was passed to athrow() and later wrapped into a RuntimeError + # (see PEP 479 for sync generators; async generators also + # have this behavior). But do this only if the exception wrapped + # by the RuntimeError is actually Stop(Async)Iteration (see + # issue29692). + if ( + isinstance(value, (StopIteration, StopAsyncIteration)) + and exc.__cause__ is value + ): + value.__traceback__ = traceback + return False + raise + except BaseException as exc: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. + if exc is not value: + raise + exc.__traceback__ = traceback + return False + raise RuntimeError("generator didn't stop after athrow()") + + +def asynccontextmanager(func): + """@asynccontextmanager decorator. + + Typical usage: + + @asynccontextmanager + async def some_async_generator(): + + try: + yield + finally: + + + This makes this: + + async with some_async_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + """ + + def helper(*args, **kwds): + return _AsyncGeneratorContextManager(func, args, kwds) + + return helper diff --git a/micropython/ucontextlib/manifest.py b/micropython/ucontextlib/manifest.py index 81e6e085..74a077dd 100644 --- a/micropython/ucontextlib/manifest.py +++ b/micropython/ucontextlib/manifest.py @@ -2,4 +2,4 @@ metadata( description="Minimal subset of contextlib for MicroPython low-memory ports", version="0.1.1" ) -module("ucontextlib.py") +package("ucontextlib") diff --git a/micropython/ucontextlib/ucontextlib.py b/micropython/ucontextlib/ucontextlib.py deleted file mode 100644 index d259f9b8..00000000 --- a/micropython/ucontextlib/ucontextlib.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Utilities for with-statement contexts. See PEP 343. - -Original source code: https://hg.python.org/cpython/file/3.4/Lib/contextlib.py - -Not implemented: - - redirect_stdout; - - ExitStack. - - closing - - supress -""" - - -class ContextDecorator(object): - "A base class or mixin that enables context managers to work as decorators." - - def _recreate_cm(self): - """Return a recreated instance of self. - - Allows an otherwise one-shot context manager like - _GeneratorContextManager to support use as - a decorator via implicit recreation. - - This is a private interface just for _GeneratorContextManager. - See issue #11647 for details. - """ - return self - - def __call__(self, func): - def inner(*args, **kwds): - with self._recreate_cm(): - return func(*args, **kwds) - - return inner - - -class _GeneratorContextManager(ContextDecorator): - """Helper for @contextmanager decorator.""" - - def __init__(self, func, *args, **kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - - def _recreate_cm(self): - # _GCM instances are one-shot context managers, so the - # CM must be recreated each time a decorated function is - # called - return self.__class__(self.func, *self.args, **self.kwds) - - def __enter__(self): - try: - return next(self.gen) - except StopIteration: - raise RuntimeError("generator didn't yield") from None - - def __exit__(self, type, value, traceback): - if type is None: - try: - next(self.gen) - except StopIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - # Need to force instantiation so we can reliably - # tell if we get the same exception back - value = type() - try: - self.gen.throw(type, value, traceback) - raise RuntimeError("generator didn't stop after throw()") - except StopIteration as exc: - # Suppress the exception *unless* it's the same exception that - # was passed to throw(). This prevents a StopIteration - # raised inside the "with" statement from being suppressed - return exc is not value - - -def contextmanager(func): - """@contextmanager decorator. - - Typical usage: - - @contextmanager - def some_generator(): - - try: - yield - finally: - - - This makes this: - - with some_generator() as : - - - equivalent to this: - - - try: - = - - finally: - - - """ - - def helper(*args, **kwds): - return _GeneratorContextManager(func, *args, **kwds) - - return helper diff --git a/micropython/ucontextlib/ucontextlib/__init__.py b/micropython/ucontextlib/ucontextlib/__init__.py new file mode 100644 index 00000000..30e7ac8f --- /dev/null +++ b/micropython/ucontextlib/ucontextlib/__init__.py @@ -0,0 +1,233 @@ +"""Utilities for with-statement contexts. See PEP 343. + +Original source code: https://hg.python.org/cpython/file/3.4/Lib/contextlib.py + +Not implemented: + - redirect_stdout; + - ExitStack. + - closing + - supress +""" + +# Provide optional dependencies (which may be installed separately). +try: + from .ucontextlib._async import asynccontextmanager +except ImportError: + pass + + +class ContextDecorator(object): + "A base class or mixin that enables context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. + """ + return self + + def __call__(self, func): + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + + return inner + + +class _GeneratorContextManager(ContextDecorator): + """Helper for @contextmanager decorator.""" + + def __init__(self, func, *args, **kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + + def _recreate_cm(self): + # _GCM instances are one-shot context managers, so the + # CM must be recreated each time a decorated function is + # called + return self.__class__(self.func, *self.args, **self.kwds) + + def __enter__(self): + try: + return next(self.gen) + except StopIteration: + raise RuntimeError("generator didn't yield") from None + + def __exit__(self, type, value, traceback): + if type is None: + try: + next(self.gen) + except StopIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = type() + try: + self.gen.throw(type, value, traceback) + raise RuntimeError("generator didn't stop after throw()") + except StopIteration as exc: + # Suppress the exception *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed + return exc is not value + + +def contextmanager(func): + """@contextmanager decorator. + + Typical usage: + + @contextmanager + def some_generator(): + + try: + yield + finally: + + + This makes this: + + with some_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + + """ + + def helper(*args, **kwds): + return _GeneratorContextManager(func, *args, **kwds) + + return helper + + +class AsyncContextDecorator(object): + "A base class or mixin that enables async context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self.""" + return self + + def __call__(self, func): + async def inner(*args, **kwds): + async with self._recreate_cm(): + return await func(*args, **kwds) + + return inner + + +class _AsyncGeneratorContextManager(AsyncContextDecorator): + """Helper for @asynccontextmanager decorator.""" + + def __init__(self, func, args, kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + + async def __aenter__(self): + # do not keep args and kwds alive unnecessarily + # they are only needed for recreation, which is not possible anymore + del self.args, self.kwds, self.func + try: + return await anext(self.gen) + except StopAsyncIteration: + raise RuntimeError("generator didn't yield") from None + + async def __aexit__(self, typ, value, traceback): + if typ is None: + try: + await anext(self.gen) + except StopAsyncIteration: + return False + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = typ() + try: + await self.gen.athrow(typ, value, traceback) + except StopAsyncIteration as exc: + # Suppress StopIteration *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed. + return exc is not value + except RuntimeError as exc: + # Don't re-raise the passed in exception. (issue27122) + if exc is value: + exc.__traceback__ = traceback + return False + # Avoid suppressing if a Stop(Async)Iteration exception + # was passed to athrow() and later wrapped into a RuntimeError + # (see PEP 479 for sync generators; async generators also + # have this behavior). But do this only if the exception wrapped + # by the RuntimeError is actually Stop(Async)Iteration (see + # issue29692). + if ( + isinstance(value, (StopIteration, StopAsyncIteration)) + and exc.__cause__ is value + ): + value.__traceback__ = traceback + return False + raise + except BaseException as exc: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. + if exc is not value: + raise + exc.__traceback__ = traceback + return False + raise RuntimeError("generator didn't stop after athrow()") + + +def asynccontextmanager(func): + """@asynccontextmanager decorator. + + Typical usage: + + @asynccontextmanager + async def some_async_generator(): + + try: + yield + finally: + + + This makes this: + + async with some_async_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + """ + + def helper(*args, **kwds): + return _AsyncGeneratorContextManager(func, args, kwds) + + return helper