diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -94,7 +94,8 @@ .. class:: ContextDecorator() - A base class that enables a context manager to also be used as a decorator. + A base class that enables a context manager to also be used as a decorator + factory. Context managers inheriting from ``ContextDecorator`` have to implement ``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional @@ -159,13 +160,25 @@ def __exit__(self, *exc): return False - .. note:: + .. versionadded:: 3.2 + + .. method:: refresh_cm() + + This method is invoked each time a call is made to a decorated function. + The default implementation just returns *self*. + 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. + underlying context manager must normally support use in multiple + ``with`` statements (preferably in a thread-safe manner). If + this is not the case, then the context manager must override this + method and return a *new* copy of the context manager on each + invocation. - .. versionadded:: 3.2 + This may mean keeping a copy of the original arguments used to + initialise the context manager. + + .. versionadded:: 3.3 + .. seealso:: diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -696,6 +696,16 @@ .. XXX addition of __slots__ to ABCs not recorded here: internal detail +contextlib +---------- + +The new :meth:`~contextlib.ContextDecorator.refresh_cm` method makes it +possible to implicitly create a new instance of the context manager for each +call to the decorated function. This means even single use context managers +can be updated to support use as decorator factories. + +(:issue:`11647`) + crypt ----- diff --git a/Lib/contextlib.py b/Lib/contextlib.py --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -9,22 +9,23 @@ 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. + def refresh_cm(self): + """Returns the context manager used to actually wrap the call to the + decorated function. - Allows otherwise one-shot context managers like - _GeneratorContextManager to support use as - decorators via implicit recreation. + The default implementation just returns *self*. - 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 + Overriding this method allows otherwise one-shot context managers + (such as those created by the @contextmanager decorator) to support + use as function decorator factories by automatically creating a + fresh instance of the context manager on each call to the function. """ return self def __call__(self, func): @wraps(func) def inner(*args, **kwds): - with self._recreate_cm(): + with self.refresh_cm(): return func(*args, **kwds) return inner @@ -36,10 +37,8 @@ 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 + def refresh_cm(self): + """Creates a new instance of the underlying context manager""" return self.__class__(self.func, *self.args, **self.kwds) def __enter__(self): diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -203,7 +203,7 @@ self.boilerPlate(lock, locked) -class mycontext(ContextDecorator): +class ReusedContextDecorator(ContextDecorator): started = False exc = None catch = False @@ -220,7 +220,7 @@ class TestContextDecorator(unittest.TestCase): def test_contextdecorator(self): - context = mycontext() + context = ReusedContextDecorator() with context as result: self.assertIs(result, context) self.assertTrue(context.started) @@ -229,7 +229,7 @@ def test_contextdecorator_with_exception(self): - context = mycontext() + context = ReusedContextDecorator() with self.assertRaisesRegex(NameError, 'foo'): with context: @@ -237,7 +237,7 @@ self.assertIsNotNone(context.exc) self.assertIs(context.exc[0], NameError) - context = mycontext() + context = ReusedContextDecorator() context.catch = True with context: raise NameError('foo') @@ -246,7 +246,7 @@ def test_decorator(self): - context = mycontext() + context = ReusedContextDecorator() @context def test(): @@ -257,7 +257,7 @@ def test_decorator_with_exception(self): - context = mycontext() + context = ReusedContextDecorator() @context def test(): @@ -272,7 +272,7 @@ def test_decorating_method(self): - context = mycontext() + context = ReusedContextDecorator() class Test(object): @@ -369,6 +369,42 @@ test('something else') self.assertEqual(state, [1, 'something else', 999]) + def test_decorator_no_refresh(self): + managers = [] + class mycontext(ContextDecorator): + def __enter__(self): + managers.append(self) + def __exit__(self, *args): + pass + @mycontext() + def test(): + pass + test() + test() + self.assertEqual(len(managers), 2) + self.assertIs(managers[0], managers[1]) + + def test_decorator_refreshed(self): + managers = [] + class mycontext(ContextDecorator): + def refresh_cm(self): + return mycontext() + def __enter__(self): + managers.append(self) + def __exit__(self, *args): + pass + decorator = mycontext() + @decorator + def test(): + pass + test() + test() + self.assertEqual(len(managers), 2) + self.assertIsNot(managers[0], decorator) + self.assertIsNot(managers[1], decorator) + self.assertIsNot(managers[0], managers[1]) + + # This is needed to make the test actually run under regrtest.py! def test_main(): diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -38,6 +38,10 @@ Library ------- +- Issue #11647: The contextlib.ContextDecorator.refresh_cm() mechanism + is now public, allowing ContextDecorator subclasses to recreate the context + manager on each call to the function. + - Issue #13152: Allow to specify a custom tabsize for expanding tabs in textwrap. Patch by John Feuerstein.