[Python-checkins] r82394 - in python/branches/py3k: Doc/library/contextlib.rst Lib/contextlib.py Lib/test/test_contextlib.py Misc/NEWS

michael.foord python-checkins at python.org
Wed Jun 30 14:17:50 CEST 2010


Author: michael.foord
Date: Wed Jun 30 14:17:50 2010
New Revision: 82394

Log:
Issue 9110. Adding ContextDecorator to contextlib. This enables the creation of APIs that act as decorators as well as context managers. contextlib.contextmanager changed to use ContextDecorator.

Modified:
   python/branches/py3k/Doc/library/contextlib.rst
   python/branches/py3k/Lib/contextlib.py
   python/branches/py3k/Lib/test/test_contextlib.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/library/contextlib.rst
==============================================================================
--- python/branches/py3k/Doc/library/contextlib.rst	(original)
+++ python/branches/py3k/Doc/library/contextlib.rst	Wed Jun 30 14:17:50 2010
@@ -51,6 +51,11 @@
    the exception has been handled, and execution will resume with the statement
    immediately following the :keyword:`with` statement.
 
+   contextmanager uses :class:`ContextDecorator` so the context managers it
+   creates can be used as decorators as well as in :keyword:`with` statements.
+
+   .. versionchanged:: 3.2
+      Use of :class:`ContextDecorator`.
 
 .. function:: closing(thing)
 
@@ -79,6 +84,58 @@
    ``page.close()`` will be called when the :keyword:`with` block is exited.
 
 
+.. class:: ContextDecorator()
+
+   A base class that enables a context manager to also be used as a decorator.
+
+   Context managers inheriting from ``ContextDecorator`` have to implement
+   ``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional
+   exception handling even when used as a decorator.
+
+   Example::
+
+      from contextlib import ContextDecorator
+
+      class mycontext(ContextDecorator):
+         def __enter__(self):
+            print('Starting')
+            return self
+
+         def __exit__(self, *exc):
+            print('Finishing')
+            return False
+
+      >>> @mycontext()
+      ... def function():
+      ...    print('The bit in the middle')
+      ...
+      >>> function()
+      Starting
+      The bit in the middle
+      Finishing
+
+      >>> with mycontext():
+      ...    print('The bit in the middle')
+      ...
+      Starting
+      The bit in the middle
+      Finishing
+
+   Existing context managers that already have a base class can be extended by
+   using ``ContextDecorator`` as a mixin class::
+
+      from contextlib import ContextDecorator
+
+      class mycontext(ContextBaseClass, ContextDecorator):
+         def __enter__(self):
+            return self
+
+         def __exit__(self, *exc):
+            return False
+
+   .. versionadded:: 3.2
+
+
 .. seealso::
 
    :pep:`0343` - The "with" statement

Modified: python/branches/py3k/Lib/contextlib.py
==============================================================================
--- python/branches/py3k/Lib/contextlib.py	(original)
+++ python/branches/py3k/Lib/contextlib.py	Wed Jun 30 14:17:50 2010
@@ -4,9 +4,20 @@
 from functools import wraps
 from warnings import warn
 
-__all__ = ["contextmanager", "closing"]
+__all__ = ["contextmanager", "closing", "ContextDecorator"]
 
-class GeneratorContextManager(object):
+
+class ContextDecorator(object):
+    "A base class or mixin that enables context managers to work as decorators."
+    def __call__(self, func):
+        @wraps(func)
+        def inner(*args, **kwds):
+            with self:
+                return func(*args, **kwds)
+        return inner
+
+
+class GeneratorContextManager(ContextDecorator):
     """Helper for @contextmanager decorator."""
 
     def __init__(self, gen):

Modified: python/branches/py3k/Lib/test/test_contextlib.py
==============================================================================
--- python/branches/py3k/Lib/test/test_contextlib.py	(original)
+++ python/branches/py3k/Lib/test/test_contextlib.py	Wed Jun 30 14:17:50 2010
@@ -202,6 +202,169 @@
                 return True
         self.boilerPlate(lock, locked)
 
+
+class mycontext(ContextDecorator):
+    started = False
+    exc = None
+    catch = False
+
+    def __enter__(self):
+        self.started = True
+        return self
+
+    def __exit__(self, *exc):
+        self.exc = exc
+        return self.catch
+
+
+class TestContextDecorator(unittest.TestCase):
+
+    def test_contextdecorator(self):
+        context = mycontext()
+        with context as result:
+            self.assertIs(result, context)
+            self.assertTrue(context.started)
+
+        self.assertEqual(context.exc, (None, None, None))
+
+
+    def test_contextdecorator_with_exception(self):
+        context = mycontext()
+
+        with self.assertRaisesRegexp(NameError, 'foo'):
+            with context:
+                raise NameError('foo')
+        self.assertIsNotNone(context.exc)
+        self.assertIs(context.exc[0], NameError)
+
+        context = mycontext()
+        context.catch = True
+        with context:
+            raise NameError('foo')
+        self.assertIsNotNone(context.exc)
+        self.assertIs(context.exc[0], NameError)
+
+
+    def test_decorator(self):
+        context = mycontext()
+
+        @context
+        def test():
+            self.assertIsNone(context.exc)
+            self.assertTrue(context.started)
+        test()
+        self.assertEqual(context.exc, (None, None, None))
+
+
+    def test_decorator_with_exception(self):
+        context = mycontext()
+
+        @context
+        def test():
+            self.assertIsNone(context.exc)
+            self.assertTrue(context.started)
+            raise NameError('foo')
+
+        with self.assertRaisesRegexp(NameError, 'foo'):
+            test()
+        self.assertIsNotNone(context.exc)
+        self.assertIs(context.exc[0], NameError)
+
+
+    def test_decorating_method(self):
+        context = mycontext()
+
+        class Test(object):
+
+            @context
+            def method(self, a, b, c=None):
+                self.a = a
+                self.b = b
+                self.c = c
+
+        # these tests are for argument passing when used as a decorator
+        test = Test()
+        test.method(1, 2)
+        self.assertEqual(test.a, 1)
+        self.assertEqual(test.b, 2)
+        self.assertEqual(test.c, None)
+
+        test = Test()
+        test.method('a', 'b', 'c')
+        self.assertEqual(test.a, 'a')
+        self.assertEqual(test.b, 'b')
+        self.assertEqual(test.c, 'c')
+
+        test = Test()
+        test.method(a=1, b=2)
+        self.assertEqual(test.a, 1)
+        self.assertEqual(test.b, 2)
+
+
+    def test_typo_enter(self):
+        class mycontext(ContextDecorator):
+            def __unter__(self):
+                pass
+            def __exit__(self, *exc):
+                pass
+
+        with self.assertRaises(AttributeError):
+            with mycontext():
+                pass
+
+
+    def test_typo_exit(self):
+        class mycontext(ContextDecorator):
+            def __enter__(self):
+                pass
+            def __uxit__(self, *exc):
+                pass
+
+        with self.assertRaises(AttributeError):
+            with mycontext():
+                pass
+
+
+    def test_contextdecorator_as_mixin(self):
+        class somecontext(object):
+            started = False
+            exc = None
+
+            def __enter__(self):
+                self.started = True
+                return self
+
+            def __exit__(self, *exc):
+                self.exc = exc
+
+        class mycontext(somecontext, ContextDecorator):
+            pass
+
+        context = mycontext()
+        @context
+        def test():
+            self.assertIsNone(context.exc)
+            self.assertTrue(context.started)
+        test()
+        self.assertEqual(context.exc, (None, None, None))
+
+
+    def test_contextmanager_as_decorator(self):
+        state = []
+        @contextmanager
+        def woohoo(y):
+            state.append(y)
+            yield
+            state.append(999)
+
+        @woohoo(1)
+        def test(x):
+            self.assertEqual(state, [1])
+            state.append(x)
+        test('something')
+        self.assertEqual(state, [1, 'something', 999])
+
+
 # This is needed to make the test actually run under regrtest.py!
 def test_main():
     support.run_unittest(__name__)

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Wed Jun 30 14:17:50 2010
@@ -460,6 +460,10 @@
 Library
 -------
 
+- Issue #9110: Addition of ContextDecorator to contextlib, for creating APIs
+  that act as both context managers and decorators. contextmanager changes
+  to use ContextDecorator.
+
 - Implement importlib.abc.SourceLoader and deprecate PyLoader and PyPycLoader
   for removal in Python 3.4.
 


More information about the Python-checkins mailing list