Skip to content

Commit

Permalink
bpo-37755: Use configured output in pydoc instead of pager (pythonGH-…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
eht16 committed Jun 8, 2024
1 parent 95f4db8 commit 2080425
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 20 deletions.
8 changes: 6 additions & 2 deletions Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2034,7 +2034,7 @@ def help(self, request, is_cli=False):
elif request in self.symbols: self.showsymbol(request)
elif request in ['True', 'False', 'None']:
# special case these keywords since they are objects too
doc(eval(request), 'Help on %s:', is_cli=is_cli)
doc(eval(request), 'Help on %s:', output=self._output, is_cli=is_cli)
elif request in self.keywords: self.showtopic(request)
elif request in self.topics: self.showtopic(request)
elif request: doc(request, 'Help on %s:', output=self._output, is_cli=is_cli)
Expand Down Expand Up @@ -2127,7 +2127,11 @@ def showtopic(self, topic, more_xrefs=''):
text = 'Related help topics: ' + ', '.join(xrefs.split()) + '\n'
wrapped_text = textwrap.wrap(text, 72)
doc += '\n%s\n' % '\n'.join(wrapped_text)
pager(doc, f'Help on {topic!s}')

if self._output is None:
pager(doc, f'Help on {topic!s}')
else:
self.output.write(doc)

def _gettopic(self, topic, more_xrefs=''):
"""Return unbuffered tuple of (topic, xrefs).
Expand Down
125 changes: 107 additions & 18 deletions Lib/test/test_pydoc/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import types
import typing
import unittest
import unittest.mock
import urllib.parse
import xml.etree
import xml.etree.ElementTree
Expand Down Expand Up @@ -658,16 +659,13 @@ def test_fail_help_output_redirect(self):

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __locals__ unexpectedly')
@unittest.mock.patch('pydoc.pager')
@requires_docstrings
def test_help_output_redirect(self):
def test_help_output_redirect(self, pager_mock):
# issue 940286, if output is set in Helper, then all output from
# Helper.help should be redirected
getpager_old = pydoc.getpager
getpager_new = lambda: (lambda x: x)
self.maxDiff = None

buf = StringIO()
helper = pydoc.Helper(output=buf)
unused, doc_loc = get_pydoc_text(pydoc_mod)
module = "test.test_pydoc.pydoc_mod"
help_header = """
Expand All @@ -677,21 +675,112 @@ def test_help_output_redirect(self):
help_header = textwrap.dedent(help_header)
expected_help_pattern = help_header + expected_text_pattern

pydoc.getpager = getpager_new
try:
with captured_output('stdout') as output, \
captured_output('stderr') as err, \
StringIO() as buf:
helper = pydoc.Helper(output=buf)
helper.help(module)
result = buf.getvalue().strip()
expected_text = expected_help_pattern % (
(doc_loc,) +
expected_text_data_docstrings +
(inspect.getabsfile(pydoc_mod),))
self.assertEqual('', output.getvalue())
self.assertEqual('', err.getvalue())
self.assertEqual(expected_text, result)

pager_mock.assert_not_called()

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __locals__ unexpectedly')
@requires_docstrings
@unittest.mock.patch('pydoc.pager')
def test_help_output_redirect_various_requests(self, pager_mock):
# issue 940286, if output is set in Helper, then all output from
# Helper.help should be redirected

def run_pydoc_for_request(request, expected_text_part):
"""Helper function to run pydoc with its output redirected"""
with captured_output('stdout') as output, \
captured_output('stderr') as err:
helper.help(module)
captured_output('stderr') as err, \
StringIO() as buf:
helper = pydoc.Helper(output=buf)
helper.help(request)
result = buf.getvalue().strip()
expected_text = expected_help_pattern % (
(doc_loc,) +
expected_text_data_docstrings +
(inspect.getabsfile(pydoc_mod),))
self.assertEqual('', output.getvalue())
self.assertEqual('', err.getvalue())
self.assertEqual(expected_text, result)
finally:
pydoc.getpager = getpager_old
self.assertEqual('', output.getvalue(), msg=f'failed on request "{request}"')
self.assertEqual('', err.getvalue(), msg=f'failed on request "{request}"')
self.assertIn(expected_text_part, result, msg=f'failed on request "{request}"')
pager_mock.assert_not_called()

self.maxDiff = None

# test for "keywords"
run_pydoc_for_request('keywords', 'Here is a list of the Python keywords.')
# test for "symbols"
run_pydoc_for_request('symbols', 'Here is a list of the punctuation symbols')
# test for "topics"
run_pydoc_for_request('topics', 'Here is a list of available topics.')
# test for "modules" skipped, see test_modules()
# test for symbol "%"
run_pydoc_for_request('%', 'The power operator')
# test for special True, False, None keywords
run_pydoc_for_request('True', 'class bool(int)')
run_pydoc_for_request('False', 'class bool(int)')
run_pydoc_for_request('None', 'class NoneType(object)')
# test for keyword "assert"
run_pydoc_for_request('assert', 'The "assert" statement')
# test for topic "TYPES"
run_pydoc_for_request('TYPES', 'The standard type hierarchy')
# test for "pydoc.Helper.help"
run_pydoc_for_request('pydoc.Helper.help', 'Help on function help in pydoc.Helper:')
# test for pydoc.Helper.help
run_pydoc_for_request(pydoc.Helper.help, 'Help on function help in module pydoc:')
# test for pydoc.Helper() instance skipped because it is always meant to be interactive

def test_showtopic(self):
with captured_stdout() as showtopic_io:
helper = pydoc.Helper()
helper.showtopic('with')
helptext = showtopic_io.getvalue()
self.assertIn('The "with" statement', helptext)

def test_fail_showtopic(self):
with captured_stdout() as showtopic_io:
helper = pydoc.Helper()
helper.showtopic('abd')
expected = "no documentation found for 'abd'"
self.assertEqual(expected, showtopic_io.getvalue().strip())

@unittest.mock.patch('pydoc.pager')
def test_fail_showtopic_output_redirect(self, pager_mock):
with StringIO() as buf:
helper = pydoc.Helper(output=buf)
helper.showtopic("abd")
expected = "no documentation found for 'abd'"
self.assertEqual(expected, buf.getvalue().strip())

pager_mock.assert_not_called()

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __locals__ unexpectedly')
@requires_docstrings
@unittest.mock.patch('pydoc.pager')
def test_showtopic_output_redirect(self, pager_mock):
# issue 940286, if output is set in Helper, then all output from
# Helper.showtopic should be redirected
self.maxDiff = None

with captured_output('stdout') as output, \
captured_output('stderr') as err, \
StringIO() as buf:
helper = pydoc.Helper(output=buf)
helper.showtopic('with')
result = buf.getvalue().strip()
self.assertEqual('', output.getvalue())
self.assertEqual('', err.getvalue())
self.assertIn('The "with" statement', result)

pager_mock.assert_not_called()

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

0 comments on commit 2080425

Please sign in to comment.