diff --git a/ucontextlib/metadata.txt b/ucontextlib/metadata.txt new file mode 100644 index 00000000..1f515fd5 --- /dev/null +++ b/ucontextlib/metadata.txt @@ -0,0 +1,5 @@ +srctype = micropython-lib +type = module +version = 0.1 +license = Python +long_desc = Minimal subset of contextlib for MicroPython low-memory ports diff --git a/ucontextlib/setup.py b/ucontextlib/setup.py new file mode 100644 index 00000000..3568fb1b --- /dev/null +++ b/ucontextlib/setup.py @@ -0,0 +1,18 @@ +import sys +# Remove current dir from sys.path, otherwise setuptools will peek up our +# module instead of system. +sys.path.pop(0) +from setuptools import setup + + +setup(name='micropython-ucontextlib', + version='0.1', + description='ucontextlib module for MicroPython', + long_description='Minimal subset of contextlib for MicroPython low-memory ports', + url='https://github.com/micropython/micropython/issues/405', + author='MicroPython Developers', + author_email='micro-python@googlegroups.com', + maintainer='MicroPython Developers', + maintainer_email='micro-python@googlegroups.com', + license='Python', + py_modules=['ucontextlib']) diff --git a/ucontextlib/tests.py b/ucontextlib/tests.py new file mode 100644 index 00000000..c5d29ecc --- /dev/null +++ b/ucontextlib/tests.py @@ -0,0 +1,36 @@ +import unittest +from ucontextlib import contextmanager + + +class ContextManagerTestCase(unittest.TestCase): + + def setUp(self): + self._history = [] + + @contextmanager + def manager(x): + self._history.append('start') + try: + yield x + finally: + self._history.append('finish') + + self._manager = manager + + def test_context_manager(self): + with self._manager(123) as x: + self.assertEqual(x, 123) + self.assertEqual(self._history, ['start', 'finish']) + + def test_context_manager_on_error(self): + exc = Exception() + try: + with self._manager(123) as x: + raise exc + except Exception as e: + self.assertEqual(exc, e) + self.assertEqual(self._history, ['start', 'finish']) + + +if __name__ == '__main__': + unittest.main() diff --git a/ucontextlib/ucontextlib.py b/ucontextlib/ucontextlib.py new file mode 100644 index 00000000..29445a02 --- /dev/null +++ b/ucontextlib/ucontextlib.py @@ -0,0 +1,106 @@ +"""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