diff --git a/Lib/pydoc.py b/Lib/pydoc.py --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -264,17 +264,17 @@ def synopsis(filename, cache={}): # XXX We probably don't need to pass in the loader here. spec = importlib.util.spec_from_file_location('__temp__', filename, loader=loader) try: module = importlib._bootstrap._load(spec) except: return None del sys.modules['__temp__'] - result = (module.__doc__ or '').splitlines()[0] + result = module.__doc__.splitlines()[0] if module.__doc__ else None # Cache the result. cache[filename] = (mtime, result) return result class ErrorDuringImport(Exception): """Errors that occurred while trying to import something to document it.""" def __init__(self, filename, exc_info): self.filename = filename @@ -2068,17 +2068,17 @@ class ModuleScanner: path = None else: try: module = importlib._bootstrap._load(spec) except ImportError: if onerror: onerror(modname) continue - desc = (module.__doc__ or '').splitlines()[0] + desc = module.__doc__.splitlines()[0] if module.__doc__ else '' path = getattr(module,'__file__',None) name = modname + ' - ' + desc if name.lower().find(key) >= 0: callback(path, modname, desc) if completer: completer() 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 @@ -1,18 +1,21 @@ import os import sys import builtins import contextlib +import importlib.util import inspect import pydoc import keyword import _pickle import pkgutil +import py_compile import re +import stat import string import test.support import time import types import unittest import urllib.parse import xml.etree import textwrap @@ -538,16 +541,28 @@ class PydocDocTest(unittest.TestCase): def test_synopsis_sourceless(self): expected = os.__doc__.splitlines()[0] filename = os.__cached__ synopsis = pydoc.synopsis(filename) self.assertEqual(synopsis, expected) + def test_synopsis_sourceless_empty_doc(self): + with test.support.temp_cwd() as test_dir: + init_path = os.path.join(test_dir, 'foomod42.py') + cached_path = importlib.util.cache_from_source(init_path) + with open(init_path, 'w') as fobj: + fobj.write("foo = 1") + py_compile.compile(init_path) + synopsis = pydoc.synopsis(init_path, {}) + self.assertIsNone(synopsis) + synopsis_cached = pydoc.synopsis(cached_path, {}) + self.assertIsNone(synopsis_cached) + def test_splitdoc_with_description(self): example_string = "I Am A Doc\n\n\nHere is my description" self.assertEqual(pydoc.splitdoc(example_string), ('I Am A Doc', '\nHere is my description')) def test_is_object_or_method(self): doc = pydoc.Doc() # Bound Method @@ -651,16 +666,32 @@ class PydocImportTest(PydocBaseTest): with self.restrict_walk_packages(path=[TESTFN]): with captured_stdout() as out: with captured_stderr() as err: pydoc.apropos('SOMEKEY') # No result, no error self.assertEqual(out.getvalue(), '') self.assertEqual(err.getvalue(), '') + def test_apropos_empty_doc(self): + pkgdir = os.path.join(TESTFN, 'walkpkg') + os.mkdir(pkgdir) + self.addCleanup(rmtree, pkgdir) + init_path = os.path.join(pkgdir, '__init__.py') + with open(init_path, 'w') as fobj: + fobj.write("foo = 1") + current_mode = stat.S_IMODE(os.stat(pkgdir).st_mode) + try: + os.chmod(pkgdir, current_mode & ~stat.S_IEXEC) + with self.restrict_walk_packages(path=[TESTFN]), captured_stdout() as stdout: + pydoc.apropos('') + self.assertIn('walkpkg', stdout.getvalue()) + finally: + os.chmod(pkgdir, current_mode) + @unittest.skip('causes undesireable side-effects (#20128)') 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