Skip to content

Commit 059def5

Browse files
committed
Close #19330 by using public classes in contextlib
- added test cases to ensure docstrings are reasonable - also updates various comments in contextlib for accuracy - identifed #19404 as an issue making it difficult to provide good help output on generator based context manager instances
1 parent 9eabac6 commit 059def5

File tree

3 files changed

+102
-46
lines changed

3 files changed

+102
-46
lines changed

Lib/contextlib.py

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ class _GeneratorContextManager(ContextDecorator):
3737
def __init__(self, func, *args, **kwds):
3838
self.gen = func(*args, **kwds)
3939
self.func, self.args, self.kwds = func, args, kwds
40+
# Issue 19330: ensure context manager instances have good docstrings
41+
doc = getattr(func, "__doc__", None)
42+
if doc is None:
43+
doc = type(self).__doc__
44+
self.__doc__ = doc
45+
# Unfortunately, this still doesn't provide good help output when
46+
# inspecting the created context manager instances, since pydoc
47+
# currently bypasses the instance docstring and shows the docstring
48+
# for the class instead.
49+
# See http://bugs.python.org/issue19404 for more details.
4050

4151
def _recreate_cm(self):
4252
# _GCM instances are one-shot context managers, so the
@@ -117,9 +127,6 @@ def helper(*args, **kwds):
117127
return helper
118128

119129

120-
# Unfortunately, this was originally published as a class, so
121-
# backwards compatibility prevents the use of the wrapper function
122-
# approach used for the other classes
123130
class closing(object):
124131
"""Context to automatically close something at the end of a block.
125132
@@ -144,8 +151,18 @@ def __enter__(self):
144151
def __exit__(self, *exc_info):
145152
self.thing.close()
146153

147-
class _RedirectStdout:
148-
"""Helper for redirect_stdout."""
154+
class redirect_stdout:
155+
"""Context manager for temporarily redirecting stdout to another file
156+
157+
# How to send help() to stderr
158+
with redirect_stdout(sys.stderr):
159+
help(dir)
160+
161+
# How to write help() to a file
162+
with open('help.txt', 'w') as f:
163+
with redirect_stdout(f):
164+
help(pow)
165+
"""
149166

150167
def __init__(self, new_target):
151168
self._new_target = new_target
@@ -163,56 +180,37 @@ def __exit__(self, exctype, excinst, exctb):
163180
self._old_target = self._sentinel
164181
sys.stdout = restore_stdout
165182

166-
# Use a wrapper function since we don't care about supporting inheritance
167-
# and a function gives much cleaner output in help()
168-
def redirect_stdout(target):
169-
"""Context manager for temporarily redirecting stdout to another file
170183

171-
# How to send help() to stderr
172-
with redirect_stdout(sys.stderr):
173-
help(dir)
174184

175-
# How to write help() to a file
176-
with open('help.txt', 'w') as f:
177-
with redirect_stdout(f):
178-
help(pow)
179-
"""
180-
return _RedirectStdout(target)
185+
class suppress:
186+
"""Context manager to suppress specified exceptions
187+
188+
After the exception is suppressed, execution proceeds with the next
189+
statement following the with statement.
181190
191+
with suppress(FileNotFoundError):
192+
os.remove(somefile)
193+
# Execution still resumes here if the file was already removed
194+
"""
182195

183-
class _SuppressExceptions:
184-
"""Helper for suppress."""
185196
def __init__(self, *exceptions):
186197
self._exceptions = exceptions
187198

188199
def __enter__(self):
189200
pass
190201

191202
def __exit__(self, exctype, excinst, exctb):
192-
# Unlike isinstance and issubclass, exception handling only
193-
# looks at the concrete type heirarchy (ignoring the instance
194-
# and subclass checking hooks). However, all exceptions are
195-
# also required to be concrete subclasses of BaseException, so
196-
# if there's a discrepancy in behaviour, we currently consider it
197-
# the fault of the strange way the exception has been defined rather
198-
# than the fact that issubclass can be customised while the
199-
# exception checks can't.
203+
# Unlike isinstance and issubclass, CPython exception handling
204+
# currently only looks at the concrete type hierarchy (ignoring
205+
# the instance and subclass checking hooks). While Guido considers
206+
# that a bug rather than a feature, it's a fairly hard one to fix
207+
# due to various internal implementation details. suppress provides
208+
# the simpler issubclass based semantics, rather than trying to
209+
# exactly reproduce the limitations of the CPython interpreter.
210+
#
200211
# See http://bugs.python.org/issue12029 for more details
201212
return exctype is not None and issubclass(exctype, self._exceptions)
202213

203-
# Use a wrapper function since we don't care about supporting inheritance
204-
# and a function gives much cleaner output in help()
205-
def suppress(*exceptions):
206-
"""Context manager to suppress specified exceptions
207-
208-
After the exception is suppressed, execution proceeds with the next
209-
statement following the with statement.
210-
211-
with suppress(FileNotFoundError):
212-
os.remove(somefile)
213-
# Execution still resumes here if the file was already removed
214-
"""
215-
return _SuppressExceptions(*exceptions)
216214

217215
# Inspired by discussions on http://bugs.python.org/issue13585
218216
class ExitStack(object):

Lib/test/test_contextlib.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@
1414

1515
class ContextManagerTestCase(unittest.TestCase):
1616

17+
def test_instance_docstring_given_function_docstring(self):
18+
# Issue 19330: ensure context manager instances have good docstrings
19+
# See http://bugs.python.org/issue19404 for why this doesn't currently
20+
# affect help() output :(
21+
def gen_with_docstring():
22+
"""This has a docstring"""
23+
yield
24+
gen_docstring = gen_with_docstring.__doc__
25+
cm_with_docstring = contextmanager(gen_with_docstring)
26+
self.assertEqual(cm_with_docstring.__doc__, gen_docstring)
27+
obj = cm_with_docstring()
28+
self.assertEqual(obj.__doc__, gen_docstring)
29+
self.assertNotEqual(obj.__doc__, type(obj).__doc__)
30+
1731
def test_contextmanager_plain(self):
1832
state = []
1933
@contextmanager
@@ -109,7 +123,11 @@ def test_contextmanager_doc_attrib(self):
109123

110124
class ClosingTestCase(unittest.TestCase):
111125

112-
# XXX This needs more work
126+
def test_instance_docs(self):
127+
# Issue 19330: ensure context manager instances have good docstrings
128+
cm_docstring = closing.__doc__
129+
obj = closing(None)
130+
self.assertEqual(obj.__doc__, cm_docstring)
113131

114132
def test_closing(self):
115133
state = []
@@ -205,6 +223,7 @@ def locked():
205223

206224

207225
class mycontext(ContextDecorator):
226+
"""Example decoration-compatible context manager for testing"""
208227
started = False
209228
exc = None
210229
catch = False
@@ -220,6 +239,12 @@ def __exit__(self, *exc):
220239

221240
class TestContextDecorator(unittest.TestCase):
222241

242+
def test_instance_docs(self):
243+
# Issue 19330: ensure context manager instances have good docstrings
244+
cm_docstring = mycontext.__doc__
245+
obj = mycontext()
246+
self.assertEqual(obj.__doc__, cm_docstring)
247+
223248
def test_contextdecorator(self):
224249
context = mycontext()
225250
with context as result:
@@ -373,6 +398,12 @@ def test(x):
373398

374399
class TestExitStack(unittest.TestCase):
375400

401+
def test_instance_docs(self):
402+
# Issue 19330: ensure context manager instances have good docstrings
403+
cm_docstring = ExitStack.__doc__
404+
obj = ExitStack()
405+
self.assertEqual(obj.__doc__, cm_docstring)
406+
376407
def test_no_resources(self):
377408
with ExitStack():
378409
pass
@@ -634,6 +665,12 @@ class Example(object): pass
634665

635666
class TestRedirectStdout(unittest.TestCase):
636667

668+
def test_instance_docs(self):
669+
# Issue 19330: ensure context manager instances have good docstrings
670+
cm_docstring = redirect_stdout.__doc__
671+
obj = redirect_stdout(None)
672+
self.assertEqual(obj.__doc__, cm_docstring)
673+
637674
def test_redirect_to_string_io(self):
638675
f = io.StringIO()
639676
msg = "Consider an API like help(), which prints directly to stdout"
@@ -671,6 +708,12 @@ def test_nested_reentry_fails(self):
671708

672709
class TestSuppress(unittest.TestCase):
673710

711+
def test_instance_docs(self):
712+
# Issue 19330: ensure context manager instances have good docstrings
713+
cm_docstring = suppress.__doc__
714+
obj = suppress()
715+
self.assertEqual(obj.__doc__, cm_docstring)
716+
674717
def test_no_result_from_enter(self):
675718
with suppress(ValueError) as enter_result:
676719
self.assertIsNone(enter_result)
@@ -683,16 +726,26 @@ def test_exact_exception(self):
683726
with suppress(TypeError):
684727
len(5)
685728

729+
def test_exception_hierarchy(self):
730+
with suppress(LookupError):
731+
'Hello'[50]
732+
733+
def test_other_exception(self):
734+
with self.assertRaises(ZeroDivisionError):
735+
with suppress(TypeError):
736+
1/0
737+
738+
def test_no_args(self):
739+
with self.assertRaises(ZeroDivisionError):
740+
with suppress():
741+
1/0
742+
686743
def test_multiple_exception_args(self):
687744
with suppress(ZeroDivisionError, TypeError):
688745
1/0
689746
with suppress(ZeroDivisionError, TypeError):
690747
len(5)
691748

692-
def test_exception_hierarchy(self):
693-
with suppress(LookupError):
694-
'Hello'[50]
695-
696749
def test_cm_is_reentrant(self):
697750
ignore_exceptions = suppress(Exception)
698751
with ignore_exceptions:

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ Core and Builtins
2121
Library
2222
-------
2323

24+
- Issue #19330: the unnecessary wrapper functions have been removed from the
25+
implementations of the new contextlib.redirect_stdout and
26+
contextlib.suppress context managers, which also ensures they provide
27+
reasonable help() output on instances
28+
2429
- Issue #18685: Restore re performance to pre-PEP 393 levels.
2530

2631
- Issue #19339: telnetlib module is now using time.monotonic() when available

0 commit comments

Comments
 (0)