diff --git a/Lib/pydoc.py b/Lib/pydoc.py --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -220,39 +220,40 @@ else: result = None return result +def _synopsis(filename): + # Look for binary suffixes first. + if any(filename.endswith(suffix) + for suffix in importlib.machinery.BYTECODE_SUFFIXES): + loader_cls = importlib.machinery.SourcelessFileLoader + elif any(filename.endswith(suffix) + for suffix in importlib.machinery.EXTENSION_SUFFIXES): + loader_cls = importlib.machinery.ExtensionFileLoader + else: + # Must be a source file. + try: + file = tokenize.open(filename) + except OSError: + # module can't be opened, so skip it + return None + # text modules can be directly examined + with file: + return source_synopsis(file) + + # Must be a binary module, which has to be imported. + loader = loader_cls('__temp__', filename) + try: + module = loader.load_module('__temp__') + except: + return None + del sys.modules['__temp__'] + return (module.__doc__ or '').splitlines()[0] + def synopsis(filename, cache={}): """Get the one-line summary out of a module file.""" mtime = os.stat(filename).st_mtime lastupdate, result = cache.get(filename, (None, None)) if lastupdate is None or lastupdate < mtime: - try: - file = tokenize.open(filename) - except OSError: - # module can't be opened, so skip it - return None - binary_suffixes = importlib.machinery.BYTECODE_SUFFIXES[:] - binary_suffixes += importlib.machinery.EXTENSION_SUFFIXES[:] - if any(filename.endswith(x) for x in binary_suffixes): - # binary modules have to be imported - file.close() - if any(filename.endswith(x) for x in - importlib.machinery.BYTECODE_SUFFIXES): - loader = importlib.machinery.SourcelessFileLoader('__temp__', - filename) - else: - loader = importlib.machinery.ExtensionFileLoader('__temp__', - filename) - try: - module = loader.load_module('__temp__') - except: - return None - result = (module.__doc__ or '').splitlines()[0] - del sys.modules['__temp__'] - else: - # text modules can be directly examined - result = source_synopsis(file) - file.close() - + result = _synopsis(filename) cache[filename] = (mtime, result) return result diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -487,6 +487,13 @@ synopsis = pydoc.synopsis(TESTFN, {}) self.assertEqual(synopsis, 'line 1: h\xe9') + def test_synopsis_sourceless(self): + expected = os.__doc__.splitlines()[0] + filename = os.__cached__ + synopsis = pydoc.synopsis(filename) + + self.assertEqual(synopsis, expected) + def test_splitdoc_with_description(self): example_string = "I Am A Doc\n\n\nHere is my description" self.assertEqual(pydoc.splitdoc(example_string), @@ -600,6 +607,50 @@ self.assertEqual(out.getvalue(), '') self.assertEqual(err.getvalue(), '') + def test_modules(self): + # See Helper.listmodules(). + num_header_lines = 2 + num_module_lines_min = 5 # Playing it safe. + num_footer_lines = 3 + expected = num_header_lines + num_module_lines_min + num_footer_lines + + output = StringIO() + helper = pydoc.Helper(output=output) + helper('modules') + result = output.getvalue().strip() + num_lines = len(result.splitlines()) + + self.assertGreaterEqual(num_lines, expected) + + def test_modules_search(self): + # See Helper.listmodules(). + expected = 'pydoc - ' + + output = StringIO() + helper = pydoc.Helper(output=output) + with captured_stdout() as help_io: + helper('modules pydoc') + result = help_io.getvalue() + + self.assertIn(expected, result) + + def test_modules_search_builtin(self): + expected = 'gc - ' + + output = StringIO() + helper = pydoc.Helper(output=output) + with captured_stdout() as help_io: + helper('modules garbage') + result = help_io.getvalue() + + self.assertTrue(result.startswith(expected)) + + def test_importfile(self): + loaded_pydoc = pydoc.importfile(pydoc.__file__) + + self.assertEqual(loaded_pydoc.__name__, 'pydoc') + self.assertEqual(loaded_pydoc.__file__, pydoc.__file__) + class TestDescriptions(unittest.TestCase): @@ -827,6 +878,7 @@ print_diffs(expected_text, result) self.fail("outputs are not equal, see diff above") + @reap_threads def test_main(): try: diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -221,6 +221,8 @@ - Issue #19782: imaplib now supports SSLContext.check_hostname and server name indication for TLS/SSL connections. +- Issue 20123: Fix pydoc.synopsis() for "binary" modules. + - Issue #19834: Support unpickling of exceptions pickled by Python 2. - Issue #19781: ftplib now supports SSLContext.check_hostname and server name