diff --git a/pdoc/docstrings.py b/pdoc/docstrings.py index b9588287..5c9dc682 100644 --- a/pdoc/docstrings.py +++ b/pdoc/docstrings.py @@ -24,6 +24,13 @@ from textwrap import indent import warnings +AnyException = (SystemExit, GeneratorExit, Exception) +"""BaseException, but excluding KeyboardInterrupt. + +Modules may raise SystemExit on import (which we want to catch), +but we don't want to catch a user's KeyboardInterrupt. +""" + @cache def convert(docstring: str, docformat: str, source_file: Path | None) -> str: @@ -32,17 +39,25 @@ def convert(docstring: str, docformat: str, source_file: Path | None) -> str: """ docformat = docformat.lower() - if any(x in docformat for x in ["google", "numpy", "restructuredtext"]): - docstring = rst(docstring, source_file) + try: + if any(x in docformat for x in ["google", "numpy", "restructuredtext"]): + docstring = rst(docstring, source_file) + + if "google" in docformat: + docstring = google(docstring) - if "google" in docformat: - docstring = google(docstring) + if "numpy" in docformat: + docstring = numpy(docstring) - if "numpy" in docformat: - docstring = numpy(docstring) + if source_file is not None and os.environ.get("PDOC_EMBED_IMAGES") != "0": + docstring = embed_images(docstring, source_file) - if source_file is not None and os.environ.get("PDOC_EMBED_IMAGES") != "0": - docstring = embed_images(docstring, source_file) + except AnyException as e: + raise RuntimeError( + 'Docstring processing failed for docstring=\n"""\n' + + docstring + + f'\n"""\n{source_file=}\n{docformat=}' + ) from e return docstring diff --git a/pdoc/extract.py b/pdoc/extract.py index fa9f2524..255f6ecc 100644 --- a/pdoc/extract.py +++ b/pdoc/extract.py @@ -61,7 +61,7 @@ def walk_specs(specs: Sequence[Path | str]) -> list[str]: modspec = importlib.util.find_spec(modname) if modspec is None: raise ModuleNotFoundError(modname) - except AnyException: + except pdoc.docstrings.AnyException: warnings.warn( f"Cannot find spec for {modname} (from {spec}):\n{traceback.format_exc()}", stacklevel=2, @@ -110,7 +110,7 @@ def parse_spec(spec: Path | str) -> str: modspec = importlib.util.find_spec(spec) if modspec is None: raise ModuleNotFoundError - except AnyException: + except pdoc.docstrings.AnyException: # Module does not exist, use local file. spec = pspec else: @@ -218,18 +218,10 @@ def load_module(module: str) -> types.ModuleType: Returns the imported module.""" try: return importlib.import_module(module) - except AnyException as e: + except pdoc.docstrings.AnyException as e: raise RuntimeError(f"Error importing {module}") from e -AnyException = (SystemExit, GeneratorExit, Exception) -"""BaseException, but excluding KeyboardInterrupt. - -Modules may raise SystemExit on import (which we want to catch), -but we don't want to catch a user's KeyboardInterrupt. -""" - - def iter_modules2(module: types.ModuleType) -> dict[str, pkgutil.ModuleInfo]: """ Returns all direct child modules of a given module. @@ -315,7 +307,7 @@ def module_mtime(modulename: str) -> float | None: try: with mock_some_common_side_effects(): spec = importlib.util.find_spec(modulename) - except AnyException: + except pdoc.docstrings.AnyException: pass else: if spec is not None and spec.origin is not None: @@ -365,7 +357,7 @@ def invalidate_caches(module_name: str) -> None: continue # some funky stuff going on - one example is typing.io, which is a class. with mock_some_common_side_effects(): importlib.reload(sys.modules[modname]) - except AnyException: + except pdoc.docstrings.AnyException: warnings.warn( f"Error reloading {modname}:\n{traceback.format_exc()}", stacklevel=2, diff --git a/test/test_docstrings.py b/test/test_docstrings.py index 8a172538..658d4d0d 100644 --- a/test/test_docstrings.py +++ b/test/test_docstrings.py @@ -37,6 +37,19 @@ def test_rst_extract_options_fuzz(s): assert not s or content or options +def test_convert_exception(monkeypatch): + def raise_(*_): + raise Exception + + monkeypatch.setattr(docstrings, "rst", raise_) + with pytest.raises(RuntimeError, match="Docstring processing failed"): + docstrings.convert( + "Valid minimal docstring without in- or output.", + "numpy", + None, + ) + + def test_rst_extract_options(): content = ( ":alpha: beta\n" diff --git a/test/test_smoke.py b/test/test_smoke.py index f8c67c49..d2b215d7 100644 --- a/test/test_smoke.py +++ b/test/test_smoke.py @@ -22,7 +22,7 @@ def test_smoke(module): try: with pdoc.extract.mock_some_common_side_effects(): importlib.import_module(module) - except pdoc.extract.AnyException: + except pdoc.docstrings.AnyException: pass else: try: