Commit 0ded3e30 authored by Nick Coghlan's avatar Nick Coghlan

Issue #11647: allow contextmanager objects to be used as decorators as...

Issue #11647: allow contextmanager objects to be used as decorators as described in the docs. Initial patch by Ysj Ray.
parent f77b74dd
...@@ -54,8 +54,12 @@ Functions provided: ...@@ -54,8 +54,12 @@ Functions provided:
the exception has been handled, and execution will resume with the statement the exception has been handled, and execution will resume with the statement
immediately following the :keyword:`with` statement. immediately following the :keyword:`with` statement.
contextmanager uses :class:`ContextDecorator` so the context managers it :func:`contextmanager` uses :class:`ContextDecorator` so the context managers
creates can be used as decorators as well as in :keyword:`with` statements. 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 .. versionchanged:: 3.2
Use of :class:`ContextDecorator`. Use of :class:`ContextDecorator`.
...@@ -155,6 +159,12 @@ Functions provided: ...@@ -155,6 +159,12 @@ Functions provided:
def __exit__(self, *exc): def __exit__(self, *exc):
return False 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 .. versionadded:: 3.2
......
...@@ -9,10 +9,23 @@ __all__ = ["contextmanager", "closing", "ContextDecorator"] ...@@ -9,10 +9,23 @@ __all__ = ["contextmanager", "closing", "ContextDecorator"]
class ContextDecorator(object): class ContextDecorator(object):
"A base class or mixin that enables context managers to work as decorators." "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): def __call__(self, func):
@wraps(func) @wraps(func)
def inner(*args, **kwds): def inner(*args, **kwds):
with self: with self._recreate_cm():
return func(*args, **kwds) return func(*args, **kwds)
return inner return inner
...@@ -20,8 +33,15 @@ class ContextDecorator(object): ...@@ -20,8 +33,15 @@ class ContextDecorator(object):
class _GeneratorContextManager(ContextDecorator): class _GeneratorContextManager(ContextDecorator):
"""Helper for @contextmanager decorator.""" """Helper for @contextmanager decorator."""
def __init__(self, gen): def __init__(self, func, *args, **kwds):
self.gen = gen 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): def __enter__(self):
try: try:
...@@ -92,7 +112,7 @@ def contextmanager(func): ...@@ -92,7 +112,7 @@ def contextmanager(func):
""" """
@wraps(func) @wraps(func)
def helper(*args, **kwds): def helper(*args, **kwds):
return _GeneratorContextManager(func(*args, **kwds)) return _GeneratorContextManager(func, *args, **kwds)
return helper return helper
......
...@@ -350,13 +350,13 @@ class TestContextDecorator(unittest.TestCase): ...@@ -350,13 +350,13 @@ class TestContextDecorator(unittest.TestCase):
def test_contextmanager_as_decorator(self): def test_contextmanager_as_decorator(self):
state = []
@contextmanager @contextmanager
def woohoo(y): def woohoo(y):
state.append(y) state.append(y)
yield yield
state.append(999) state.append(999)
state = []
@woohoo(1) @woohoo(1)
def test(x): def test(x):
self.assertEqual(state, [1]) self.assertEqual(state, [1])
...@@ -364,6 +364,11 @@ class TestContextDecorator(unittest.TestCase): ...@@ -364,6 +364,11 @@ class TestContextDecorator(unittest.TestCase):
test('something') test('something')
self.assertEqual(state, [1, 'something', 999]) 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! # This is needed to make the test actually run under regrtest.py!
def test_main(): def test_main():
......
...@@ -14,8 +14,8 @@ from test.support import run_unittest ...@@ -14,8 +14,8 @@ from test.support import run_unittest
class MockContextManager(_GeneratorContextManager): class MockContextManager(_GeneratorContextManager):
def __init__(self, gen): def __init__(self, func, *args, **kwds):
_GeneratorContextManager.__init__(self, gen) super().__init__(func, *args, **kwds)
self.enter_called = False self.enter_called = False
self.exit_called = False self.exit_called = False
self.exit_args = None self.exit_args = None
...@@ -33,7 +33,7 @@ class MockContextManager(_GeneratorContextManager): ...@@ -33,7 +33,7 @@ class MockContextManager(_GeneratorContextManager):
def mock_contextmanager(func): def mock_contextmanager(func):
def helper(*args, **kwds): def helper(*args, **kwds):
return MockContextManager(func(*args, **kwds)) return MockContextManager(func, *args, **kwds)
return helper return helper
......
...@@ -704,6 +704,7 @@ Burton Radons ...@@ -704,6 +704,7 @@ Burton Radons
Brodie Rao Brodie Rao
Antti Rasinen Antti Rasinen
Sridhar Ratnakumar Sridhar Ratnakumar
Ysj Ray
Eric Raymond Eric Raymond
Edward K. Ream Edward K. Ream
Chris Rebert Chris Rebert
......
...@@ -83,6 +83,10 @@ Core and Builtins ...@@ -83,6 +83,10 @@ Core and Builtins
Library 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. - logging: don't define QueueListener if Python has no thread support.
- functools.cmp_to_key() now works with collections.Hashable(). - functools.cmp_to_key() now works with collections.Hashable().
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment