Skip to content

Commit

Permalink
Close sphinx-doc#9555: autosummary: Improve error messages on failure…
Browse files Browse the repository at this point in the history
… to load target object
  • Loading branch information
tk0miya committed Dec 30, 2021
1 parent 8ddf3f0 commit 9039991
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGES
Expand Up @@ -23,6 +23,7 @@ Features added
``__all__`` attribute if :confval:`autosummary_ignore_module_all` is set to
``False``. The default behaviour is unchanged. Autogen also now supports
this behavior with the ``--respect-module-all`` switch.
* #9555: autosummary: Improve error messages on failure to load target object
* #9800: extlinks: Emit warning if a hardcoded link is replaceable
by an extlink, suggesting a replacement.
* #9961: html: Support nested <kbd> HTML elements in other HTML builders
Expand Down
85 changes: 60 additions & 25 deletions sphinx/ext/autosummary/__init__.py
Expand Up @@ -61,7 +61,7 @@
from inspect import Parameter
from os import path
from types import ModuleType
from typing import Any, Dict, List, Optional, Tuple, Type, cast
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, cast

from docutils import nodes
from docutils.nodes import Element, Node, system_message
Expand Down Expand Up @@ -306,15 +306,18 @@ def run(self) -> List[Node]:
def import_by_name(self, name: str, prefixes: List[str]) -> Tuple[str, Any, Any, str]:
with mock(self.config.autosummary_mock_imports):
try:
return import_by_name(name, prefixes)
except ImportError as exc:
return import_by_name(name, prefixes, grouped_exception=True)
except ImportExceptionGroup as exc:
# check existence of instance attribute
try:
return import_ivar_by_name(name, prefixes)
except ImportError:
pass
except ImportError as exc2:
if exc2.__cause__:
errors: List[BaseException] = exc.exceptions + [exc2.__cause__]
else:
errors = exc.exceptions + [exc2]

raise exc # re-raise ImportError if instance attribute not found
raise ImportExceptionGroup(exc.args[0], errors)

def create_documenter(self, app: Sphinx, obj: Any,
parent: Any, full_name: str) -> "Documenter":
Expand Down Expand Up @@ -344,9 +347,10 @@ def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]:

try:
real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes)
except ImportError:
logger.warning(__('autosummary: failed to import %s'), name,
location=self.get_location())
except ImportExceptionGroup as exc:
errors = list(set("* %s: %s" % (type(e).__name__, e) for e in exc.exceptions))
logger.warning(__('autosummary: failed to import %s.\nPossible hints:\n%s'),
name, '\n'.join(errors), location=self.get_location())
continue

self.bridge.result = StringList() # initialize for each documenter
Expand Down Expand Up @@ -620,6 +624,18 @@ def limited_join(sep: str, items: List[str], max_chars: int = 30,

# -- Importing items -----------------------------------------------------------


class ImportExceptionGroup(Exception):
"""Exceptions raised during importing the target objects.
It contains an error messages and a list of exceptions as its arguments.
"""

def __init__(self, message: Optional[str], exceptions: Sequence[BaseException]):
super().__init__(message)
self.exceptions = list(exceptions)


def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
"""
Obtain current Python import prefixes (for `import_by_name`)
Expand All @@ -641,26 +657,38 @@ def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
return prefixes


def import_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]:
def import_by_name(name: str, prefixes: List[str] = [None], grouped_exception: bool = False
) -> Tuple[str, Any, Any, str]:
"""Import a Python object that has the given *name*, under one of the
*prefixes*. The first name that succeeds is used.
"""
tried = []
errors: List[ImportExceptionGroup] = []
for prefix in prefixes:
try:
if prefix:
prefixed_name = '.'.join([prefix, name])
else:
prefixed_name = name
obj, parent, modname = _import_by_name(prefixed_name)
obj, parent, modname = _import_by_name(prefixed_name, grouped_exception)
return prefixed_name, obj, parent, modname
except ImportError:
tried.append(prefixed_name)
raise ImportError('no module named %s' % ' or '.join(tried))
except ImportExceptionGroup as exc:
tried.append(prefixed_name)
errors.append(exc)

if grouped_exception:
exceptions: List[BaseException] = sum((e.exceptions for e in errors), [])
raise ImportExceptionGroup('no module named %s' % ' or '.join(tried), exceptions)
else:
raise ImportError('no module named %s' % ' or '.join(tried))


def _import_by_name(name: str) -> Tuple[Any, Any, str]:
def _import_by_name(name: str, grouped_exception: bool = False) -> Tuple[Any, Any, str]:
"""Import a Python object given its full name."""
errors: List[BaseException] = []

try:
name_parts = name.split('.')

Expand All @@ -670,8 +698,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
try:
mod = import_module(modname)
return getattr(mod, name_parts[-1]), mod, modname
except (ImportError, IndexError, AttributeError):
pass
except (ImportError, IndexError, AttributeError) as exc:
errors.append(exc.__cause__ or exc)

# ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
last_j = 0
Expand All @@ -681,8 +709,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
modname = '.'.join(name_parts[:j])
try:
import_module(modname)
except ImportError:
continue
except ImportError as exc:
errors.append(exc.__cause__ or exc)

if modname in sys.modules:
break
Expand All @@ -696,25 +724,32 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
return obj, parent, modname
else:
return sys.modules[modname], None, modname
except (ValueError, ImportError, AttributeError, KeyError) as e:
raise ImportError(*e.args) from e
except (ValueError, ImportError, AttributeError, KeyError) as exc:
errors.append(exc)
if grouped_exception:
raise ImportExceptionGroup('', errors)
else:
raise ImportError(*exc.args) from exc


def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]:
def import_ivar_by_name(name: str, prefixes: List[str] = [None],
grouped_exception: bool = False) -> Tuple[str, Any, Any, str]:
"""Import an instance variable that has the given *name*, under one of the
*prefixes*. The first name that succeeds is used.
"""
try:
name, attr = name.rsplit(".", 1)
real_name, obj, parent, modname = import_by_name(name, prefixes)
real_name, obj, parent, modname = import_by_name(name, prefixes, grouped_exception)
qualname = real_name.replace(modname + ".", "")
analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname))
analyzer.analyze()
# check for presence in `annotations` to include dataclass attributes
if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations:
return real_name + "." + attr, INSTANCEATTR, obj, modname
except (ImportError, ValueError, PycodeError):
pass
except (ImportError, ValueError, PycodeError) as exc:
raise ImportError from exc
except ImportExceptionGroup:
raise # pass through it as is

raise ImportError

Expand All @@ -739,8 +774,8 @@ def run(self) -> Tuple[List[Node], List[system_message]]:
try:
# try to import object by name
prefixes = get_import_prefixes_from_env(self.env)
import_by_name(pending_xref['reftarget'], prefixes)
except ImportError:
import_by_name(pending_xref['reftarget'], prefixes, grouped_exception=True)
except ImportExceptionGroup:
literal = cast(nodes.literal, pending_xref[0])
objects[0] = nodes.emphasis(self.rawtext, literal.astext(),
classes=literal['classes'])
Expand Down
27 changes: 18 additions & 9 deletions sphinx/ext/autosummary/generate.py
Expand Up @@ -41,7 +41,8 @@
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.ext.autodoc import Documenter
from sphinx.ext.autodoc.importer import import_module
from sphinx.ext.autosummary import get_documenter, import_by_name, import_ivar_by_name
from sphinx.ext.autosummary import (ImportExceptionGroup, get_documenter, import_by_name,
import_ivar_by_name)
from sphinx.locale import __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.registry import SphinxComponentRegistry
Expand Down Expand Up @@ -430,15 +431,22 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
ensuredir(path)

try:
name, obj, parent, modname = import_by_name(entry.name)
name, obj, parent, modname = import_by_name(entry.name, grouped_exception=True)
qualname = name.replace(modname + ".", "")
except ImportError as e:
except ImportExceptionGroup as exc:
try:
# try to importl as an instance attribute
# try to import as an instance attribute
name, obj, parent, modname = import_ivar_by_name(entry.name)
qualname = name.replace(modname + ".", "")
except ImportError:
logger.warning(__('[autosummary] failed to import %r: %s') % (entry.name, e))
except ImportError as exc2:
if exc2.__cause__:
exceptions: List[BaseException] = exc.exceptions + [exc2.__cause__]
else:
exceptions = exc.exceptions + [exc2]

errors = list(set("* %s: %s" % (type(e).__name__, e) for e in exceptions))
logger.warning(__('[autosummary] failed to import %s.\nPossible hints:\n%s'),
entry.name, '\n'.join(errors))
continue

context: Dict[str, Any] = {}
Expand Down Expand Up @@ -500,13 +508,14 @@ def find_autosummary_in_docstring(name: str, module: str = None, filename: str =
RemovedInSphinx50Warning, stacklevel=2)

try:
real_name, obj, parent, modname = import_by_name(name)
real_name, obj, parent, modname = import_by_name(name, grouped_exception=True)
lines = pydoc.getdoc(obj).splitlines()
return find_autosummary_in_lines(lines, module=name, filename=filename)
except AttributeError:
pass
except ImportError as e:
print("Failed to import '%s': %s" % (name, e))
except ImportExceptionGroup as exc:
errors = list(set("* %s: %s" % (type(e).__name__, e) for e in exc.exceptions))
print('Failed to import %s.\nPossible hints:\n%s' % (name, '\n'.join(errors)))
except SystemExit:
print("Failed to import '%s'; the module executes module level "
"statement and it might call sys.exit()." % name)
Expand Down

0 comments on commit 9039991

Please sign in to comment.