Issue #11647: allow contextmanager objects to be used as decorators as described...
authorNick Coghlan <ncoghlan@gmail.com>
Thu, 5 May 2011 13:49:25 +0000 (23:49 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Thu, 5 May 2011 13:49:25 +0000 (23:49 +1000)
Doc/library/contextlib.rst
Lib/contextlib.py
Lib/test/test_contextlib.py
Lib/test/test_with.py
Misc/ACKS
Misc/NEWS

index a35ea569c17221724c944aa79265778fb87d45cd..e8dc17fed324dc9e9537d49a5f57d213ec3fc05b 100644 (file)
@@ -54,8 +54,12 @@ Functions provided:
    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.
+   :func:`contextmanager` uses :class:`ContextDecorator` so the context managers
+   it creates can be used as decorators as well as in :keyword:`with` statements.
+   When used as a decorator, a new generator instance is implicitly created on
+   each function call (this allows the otherwise "one-shot" context managers
+   created by :func:`contextmanager` to meet the requirement that context
+   managers support multiple invocations in order to be used as decorators).
 
    .. versionchanged:: 3.2
       Use of :class:`ContextDecorator`.
@@ -155,6 +159,12 @@ Functions provided:
           def __exit__(self, *exc):
               return False
 
+   .. note::
+      As the decorated function must be able to be called multiple times, the
+      underlying context manager must support use in multiple :keyword:`with`
+      statements. If this is not the case, then the original construct with the
+      explicit :keyword:`with` statement inside the function should be used.
+
    .. versionadded:: 3.2
 
 
index 4633cff7a48277881c1912ab48b42dc29784eca1..e90fc4181a39b406a94abddc409eede03fd8fd67 100644 (file)
@@ -9,10 +9,23 @@ __all__ = ["contextmanager", "closing", "ContextDecorator"]
 
 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 otherwise one-shot context managers like
+        _GeneratorContextManager to support use as
+        decorators via implicit recreation.
+        
+        Note: this is a private interface just for _GCM in 3.2 but will be
+        renamed and documented for third party use in 3.3
+        """
+        return self
+
     def __call__(self, func):
         @wraps(func)
         def inner(*args, **kwds):
-            with self:
+            with self._recreate_cm():
                 return func(*args, **kwds)
         return inner
 
@@ -20,8 +33,15 @@ class ContextDecorator(object):
 class _GeneratorContextManager(ContextDecorator):
     """Helper for @contextmanager decorator."""
 
-    def __init__(self, gen):
-        self.gen = gen
+    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:
@@ -92,7 +112,7 @@ def contextmanager(func):
     """
     @wraps(func)
     def helper(*args, **kwds):
-        return _GeneratorContextManager(func(*args, **kwds))
+        return _GeneratorContextManager(func, *args, **kwds)
     return helper
 
 
index d6bb5b818e138e772c4f6312ba55a6c808aed5cc..6e38305123de647e6c7ea607111001a095eaa008 100644 (file)
@@ -350,13 +350,13 @@ class TestContextDecorator(unittest.TestCase):
 
 
     def test_contextmanager_as_decorator(self):
-        state = []
         @contextmanager
         def woohoo(y):
             state.append(y)
             yield
             state.append(999)
 
+        state = []
         @woohoo(1)
         def test(x):
             self.assertEqual(state, [1])
@@ -364,6 +364,11 @@ class TestContextDecorator(unittest.TestCase):
         test('something')
         self.assertEqual(state, [1, 'something', 999])
 
+        # Issue #11647: Ensure the decorated function is 'reusable'
+        state = []
+        test('something else')
+        self.assertEqual(state, [1, 'something else', 999])
+
 
 # This is needed to make the test actually run under regrtest.py!
 def test_main():
index a9d374b3244d1c4d1bf538743ecccd5355c07b3f..e8cc8c056ec6c53d447bf0ad0c130fc7950049aa 100644 (file)
@@ -14,8 +14,8 @@ from test.support import run_unittest
 
 
 class MockContextManager(_GeneratorContextManager):
-    def __init__(self, gen):
-        _GeneratorContextManager.__init__(self, gen)
+    def __init__(self, func, *args, **kwds):
+        super().__init__(func, *args, **kwds)
         self.enter_called = False
         self.exit_called = False
         self.exit_args = None
@@ -33,7 +33,7 @@ class MockContextManager(_GeneratorContextManager):
 
 def mock_contextmanager(func):
     def helper(*args, **kwds):
-        return MockContextManager(func(*args, **kwds))
+        return MockContextManager(func, *args, **kwds)
     return helper
 
 
index c3f4e94ba9397c24a46c555d89eb6e645f7d3ffb..0443e9374c42a4191922770e7a455e3ed8ae261a 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -704,6 +704,7 @@ Burton Radons
 Brodie Rao
 Antti Rasinen
 Sridhar Ratnakumar
+Ysj Ray
 Eric Raymond
 Edward K. Ream
 Chris Rebert
index ca22acf2abd0a6cfefa7da54f3ef9a2e391d73c1..5fb153d5ecf9727abcdf8594fac6ad2f76149e85 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -83,6 +83,10 @@ Core and Builtins
 Library
 -------
 
+- Issue #11647: objects created using contextlib.contextmanager now support
+  more than one call to the function when used as a decorator. Initial patch
+  by Ysj Ray.
+
 - logging: don't define QueueListener if Python has no thread support.
 
 - functools.cmp_to_key() now works with collections.Hashable().