Skip to content

Commit 2080425

Browse files
authored
bpo-37755: Use configured output in pydoc instead of pager (GH-15105)
If the Helper() class was initialized with an output, the topics, keywords and symbols help still use the pager instead of the output. Change the behavior so the output is used if available while keeping the previous behavior if no output was configured.
1 parent 95f4db8 commit 2080425

File tree

3 files changed

+116
-20
lines changed

3 files changed

+116
-20
lines changed

Lib/pydoc.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -2034,7 +2034,7 @@ def help(self, request, is_cli=False):
20342034
elif request in self.symbols: self.showsymbol(request)
20352035
elif request in ['True', 'False', 'None']:
20362036
# special case these keywords since they are objects too
2037-
doc(eval(request), 'Help on %s:', is_cli=is_cli)
2037+
doc(eval(request), 'Help on %s:', output=self._output, is_cli=is_cli)
20382038
elif request in self.keywords: self.showtopic(request)
20392039
elif request in self.topics: self.showtopic(request)
20402040
elif request: doc(request, 'Help on %s:', output=self._output, is_cli=is_cli)
@@ -2127,7 +2127,11 @@ def showtopic(self, topic, more_xrefs=''):
21272127
text = 'Related help topics: ' + ', '.join(xrefs.split()) + '\n'
21282128
wrapped_text = textwrap.wrap(text, 72)
21292129
doc += '\n%s\n' % '\n'.join(wrapped_text)
2130-
pager(doc, f'Help on {topic!s}')
2130+
2131+
if self._output is None:
2132+
pager(doc, f'Help on {topic!s}')
2133+
else:
2134+
self.output.write(doc)
21312135

21322136
def _gettopic(self, topic, more_xrefs=''):
21332137
"""Return unbuffered tuple of (topic, xrefs).

Lib/test/test_pydoc/test_pydoc.py

+107-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import types
1818
import typing
1919
import unittest
20+
import unittest.mock
2021
import urllib.parse
2122
import xml.etree
2223
import xml.etree.ElementTree
@@ -658,16 +659,13 @@ def test_fail_help_output_redirect(self):
658659

659660
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
660661
'trace function introduces __locals__ unexpectedly')
662+
@unittest.mock.patch('pydoc.pager')
661663
@requires_docstrings
662-
def test_help_output_redirect(self):
664+
def test_help_output_redirect(self, pager_mock):
663665
# issue 940286, if output is set in Helper, then all output from
664666
# Helper.help should be redirected
665-
getpager_old = pydoc.getpager
666-
getpager_new = lambda: (lambda x: x)
667667
self.maxDiff = None
668668

669-
buf = StringIO()
670-
helper = pydoc.Helper(output=buf)
671669
unused, doc_loc = get_pydoc_text(pydoc_mod)
672670
module = "test.test_pydoc.pydoc_mod"
673671
help_header = """
@@ -677,21 +675,112 @@ def test_help_output_redirect(self):
677675
help_header = textwrap.dedent(help_header)
678676
expected_help_pattern = help_header + expected_text_pattern
679677

680-
pydoc.getpager = getpager_new
681-
try:
678+
with captured_output('stdout') as output, \
679+
captured_output('stderr') as err, \
680+
StringIO() as buf:
681+
helper = pydoc.Helper(output=buf)
682+
helper.help(module)
683+
result = buf.getvalue().strip()
684+
expected_text = expected_help_pattern % (
685+
(doc_loc,) +
686+
expected_text_data_docstrings +
687+
(inspect.getabsfile(pydoc_mod),))
688+
self.assertEqual('', output.getvalue())
689+
self.assertEqual('', err.getvalue())
690+
self.assertEqual(expected_text, result)
691+
692+
pager_mock.assert_not_called()
693+
694+
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
695+
'trace function introduces __locals__ unexpectedly')
696+
@requires_docstrings
697+
@unittest.mock.patch('pydoc.pager')
698+
def test_help_output_redirect_various_requests(self, pager_mock):
699+
# issue 940286, if output is set in Helper, then all output from
700+
# Helper.help should be redirected
701+
702+
def run_pydoc_for_request(request, expected_text_part):
703+
"""Helper function to run pydoc with its output redirected"""
682704
with captured_output('stdout') as output, \
683-
captured_output('stderr') as err:
684-
helper.help(module)
705+
captured_output('stderr') as err, \
706+
StringIO() as buf:
707+
helper = pydoc.Helper(output=buf)
708+
helper.help(request)
685709
result = buf.getvalue().strip()
686-
expected_text = expected_help_pattern % (
687-
(doc_loc,) +
688-
expected_text_data_docstrings +
689-
(inspect.getabsfile(pydoc_mod),))
690-
self.assertEqual('', output.getvalue())
691-
self.assertEqual('', err.getvalue())
692-
self.assertEqual(expected_text, result)
693-
finally:
694-
pydoc.getpager = getpager_old
710+
self.assertEqual('', output.getvalue(), msg=f'failed on request "{request}"')
711+
self.assertEqual('', err.getvalue(), msg=f'failed on request "{request}"')
712+
self.assertIn(expected_text_part, result, msg=f'failed on request "{request}"')
713+
pager_mock.assert_not_called()
714+
715+
self.maxDiff = None
716+
717+
# test for "keywords"
718+
run_pydoc_for_request('keywords', 'Here is a list of the Python keywords.')
719+
# test for "symbols"
720+
run_pydoc_for_request('symbols', 'Here is a list of the punctuation symbols')
721+
# test for "topics"
722+
run_pydoc_for_request('topics', 'Here is a list of available topics.')
723+
# test for "modules" skipped, see test_modules()
724+
# test for symbol "%"
725+
run_pydoc_for_request('%', 'The power operator')
726+
# test for special True, False, None keywords
727+
run_pydoc_for_request('True', 'class bool(int)')
728+
run_pydoc_for_request('False', 'class bool(int)')
729+
run_pydoc_for_request('None', 'class NoneType(object)')
730+
# test for keyword "assert"
731+
run_pydoc_for_request('assert', 'The "assert" statement')
732+
# test for topic "TYPES"
733+
run_pydoc_for_request('TYPES', 'The standard type hierarchy')
734+
# test for "pydoc.Helper.help"
735+
run_pydoc_for_request('pydoc.Helper.help', 'Help on function help in pydoc.Helper:')
736+
# test for pydoc.Helper.help
737+
run_pydoc_for_request(pydoc.Helper.help, 'Help on function help in module pydoc:')
738+
# test for pydoc.Helper() instance skipped because it is always meant to be interactive
739+
740+
def test_showtopic(self):
741+
with captured_stdout() as showtopic_io:
742+
helper = pydoc.Helper()
743+
helper.showtopic('with')
744+
helptext = showtopic_io.getvalue()
745+
self.assertIn('The "with" statement', helptext)
746+
747+
def test_fail_showtopic(self):
748+
with captured_stdout() as showtopic_io:
749+
helper = pydoc.Helper()
750+
helper.showtopic('abd')
751+
expected = "no documentation found for 'abd'"
752+
self.assertEqual(expected, showtopic_io.getvalue().strip())
753+
754+
@unittest.mock.patch('pydoc.pager')
755+
def test_fail_showtopic_output_redirect(self, pager_mock):
756+
with StringIO() as buf:
757+
helper = pydoc.Helper(output=buf)
758+
helper.showtopic("abd")
759+
expected = "no documentation found for 'abd'"
760+
self.assertEqual(expected, buf.getvalue().strip())
761+
762+
pager_mock.assert_not_called()
763+
764+
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
765+
'trace function introduces __locals__ unexpectedly')
766+
@requires_docstrings
767+
@unittest.mock.patch('pydoc.pager')
768+
def test_showtopic_output_redirect(self, pager_mock):
769+
# issue 940286, if output is set in Helper, then all output from
770+
# Helper.showtopic should be redirected
771+
self.maxDiff = None
772+
773+
with captured_output('stdout') as output, \
774+
captured_output('stderr') as err, \
775+
StringIO() as buf:
776+
helper = pydoc.Helper(output=buf)
777+
helper.showtopic('with')
778+
result = buf.getvalue().strip()
779+
self.assertEqual('', output.getvalue())
780+
self.assertEqual('', err.getvalue())
781+
self.assertIn('The "with" statement', result)
782+
783+
pager_mock.assert_not_called()
695784

696785
def test_lambda_with_return_annotation(self):
697786
func = lambda a, b, c: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`!help` and :meth:`!showtopic` methods now respect a
2+
configured *output* argument to :class:`!pydoc.Helper` and not use the
3+
pager in such cases. Patch by Enrico Tröger.

0 commit comments

Comments
 (0)