Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0ded3e3

Browse files
committed
Issue #11647: allow contextmanager objects to be used as decorators as described in the docs. Initial patch by Ysj Ray.
1 parent f77b74d commit 0ded3e3

6 files changed

Lines changed: 50 additions & 10 deletions

File tree

Doc/library/contextlib.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ Functions provided:
5454
the exception has been handled, and execution will resume with the statement
5555
immediately following the :keyword:`with` statement.
5656

57-
contextmanager uses :class:`ContextDecorator` so the context managers it
58-
creates can be used as decorators as well as in :keyword:`with` statements.
57+
:func:`contextmanager` uses :class:`ContextDecorator` so the context managers
58+
it creates can be used as decorators as well as in :keyword:`with` statements.
59+
When used as a decorator, a new generator instance is implicitly created on
60+
each function call (this allows the otherwise "one-shot" context managers
61+
created by :func:`contextmanager` to meet the requirement that context
62+
managers support multiple invocations in order to be used as decorators).
5963

6064
.. versionchanged:: 3.2
6165
Use of :class:`ContextDecorator`.
@@ -155,6 +159,12 @@ Functions provided:
155159
def __exit__(self, *exc):
156160
return False
157161

162+
.. note::
163+
As the decorated function must be able to be called multiple times, the
164+
underlying context manager must support use in multiple :keyword:`with`
165+
statements. If this is not the case, then the original construct with the
166+
explicit :keyword:`with` statement inside the function should be used.
167+
158168
.. versionadded:: 3.2
159169

160170

Lib/contextlib.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,39 @@
99

1010
class ContextDecorator(object):
1111
"A base class or mixin that enables context managers to work as decorators."
12+
13+
def _recreate_cm(self):
14+
"""Return a recreated instance of self.
15+
16+
Allows otherwise one-shot context managers like
17+
_GeneratorContextManager to support use as
18+
decorators via implicit recreation.
19+
20+
Note: this is a private interface just for _GCM in 3.2 but will be
21+
renamed and documented for third party use in 3.3
22+
"""
23+
return self
24+
1225
def __call__(self, func):
1326
@wraps(func)
1427
def inner(*args, **kwds):
15-
with self:
28+
with self._recreate_cm():
1629
return func(*args, **kwds)
1730
return inner
1831

1932

2033
class _GeneratorContextManager(ContextDecorator):
2134
"""Helper for @contextmanager decorator."""
2235

23-
def __init__(self, gen):
24-
self.gen = gen
36+
def __init__(self, func, *args, **kwds):
37+
self.gen = func(*args, **kwds)
38+
self.func, self.args, self.kwds = func, args, kwds
39+
40+
def _recreate_cm(self):
41+
# _GCM instances are one-shot context managers, so the
42+
# CM must be recreated each time a decorated function is
43+
# called
44+
return self.__class__(self.func, *self.args, **self.kwds)
2545

2646
def __enter__(self):
2747
try:
@@ -92,7 +112,7 @@ def some_generator(<arguments>):
92112
"""
93113
@wraps(func)
94114
def helper(*args, **kwds):
95-
return _GeneratorContextManager(func(*args, **kwds))
115+
return _GeneratorContextManager(func, *args, **kwds)
96116
return helper
97117

98118

Lib/test/test_contextlib.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,20 +350,25 @@ def test():
350350

351351

352352
def test_contextmanager_as_decorator(self):
353-
state = []
354353
@contextmanager
355354
def woohoo(y):
356355
state.append(y)
357356
yield
358357
state.append(999)
359358

359+
state = []
360360
@woohoo(1)
361361
def test(x):
362362
self.assertEqual(state, [1])
363363
state.append(x)
364364
test('something')
365365
self.assertEqual(state, [1, 'something', 999])
366366

367+
# Issue #11647: Ensure the decorated function is 'reusable'
368+
state = []
369+
test('something else')
370+
self.assertEqual(state, [1, 'something else', 999])
371+
367372

368373
# This is needed to make the test actually run under regrtest.py!
369374
def test_main():

Lib/test/test_with.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515

1616
class MockContextManager(_GeneratorContextManager):
17-
def __init__(self, gen):
18-
_GeneratorContextManager.__init__(self, gen)
17+
def __init__(self, func, *args, **kwds):
18+
super().__init__(func, *args, **kwds)
1919
self.enter_called = False
2020
self.exit_called = False
2121
self.exit_args = None
@@ -33,7 +33,7 @@ def __exit__(self, type, value, traceback):
3333

3434
def mock_contextmanager(func):
3535
def helper(*args, **kwds):
36-
return MockContextManager(func(*args, **kwds))
36+
return MockContextManager(func, *args, **kwds)
3737
return helper
3838

3939

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ Burton Radons
704704
Brodie Rao
705705
Antti Rasinen
706706
Sridhar Ratnakumar
707+
Ysj Ray
707708
Eric Raymond
708709
Edward K. Ream
709710
Chris Rebert

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ Core and Builtins
8383
Library
8484
-------
8585

86+
- Issue #11647: objects created using contextlib.contextmanager now support
87+
more than one call to the function when used as a decorator. Initial patch
88+
by Ysj Ray.
89+
8690
- logging: don't define QueueListener if Python has no thread support.
8791

8892
- functools.cmp_to_key() now works with collections.Hashable().

0 commit comments

Comments
 (0)