mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Close #9555: autosummary: Improve error messages on failure to load target object
This commit is contained in:
parent
8ddf3f09c6
commit
9039991a3b
1
CHANGES
1
CHANGES
@ -23,6 +23,7 @@ Features added
|
|||||||
``__all__`` attribute if :confval:`autosummary_ignore_module_all` is set to
|
``__all__`` attribute if :confval:`autosummary_ignore_module_all` is set to
|
||||||
``False``. The default behaviour is unchanged. Autogen also now supports
|
``False``. The default behaviour is unchanged. Autogen also now supports
|
||||||
this behavior with the ``--respect-module-all`` switch.
|
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
|
* #9800: extlinks: Emit warning if a hardcoded link is replaceable
|
||||||
by an extlink, suggesting a replacement.
|
by an extlink, suggesting a replacement.
|
||||||
* #9961: html: Support nested <kbd> HTML elements in other HTML builders
|
* #9961: html: Support nested <kbd> HTML elements in other HTML builders
|
||||||
|
@ -61,7 +61,7 @@ import warnings
|
|||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
from os import path
|
from os import path
|
||||||
from types import ModuleType
|
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 import nodes
|
||||||
from docutils.nodes import Element, Node, system_message
|
from docutils.nodes import Element, Node, system_message
|
||||||
@ -306,15 +306,18 @@ class Autosummary(SphinxDirective):
|
|||||||
def import_by_name(self, name: str, prefixes: List[str]) -> Tuple[str, Any, Any, str]:
|
def import_by_name(self, name: str, prefixes: List[str]) -> Tuple[str, Any, Any, str]:
|
||||||
with mock(self.config.autosummary_mock_imports):
|
with mock(self.config.autosummary_mock_imports):
|
||||||
try:
|
try:
|
||||||
return import_by_name(name, prefixes)
|
return import_by_name(name, prefixes, grouped_exception=True)
|
||||||
except ImportError as exc:
|
except ImportExceptionGroup as exc:
|
||||||
# check existence of instance attribute
|
# check existence of instance attribute
|
||||||
try:
|
try:
|
||||||
return import_ivar_by_name(name, prefixes)
|
return import_ivar_by_name(name, prefixes)
|
||||||
except ImportError:
|
except ImportError as exc2:
|
||||||
pass
|
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,
|
def create_documenter(self, app: Sphinx, obj: Any,
|
||||||
parent: Any, full_name: str) -> "Documenter":
|
parent: Any, full_name: str) -> "Documenter":
|
||||||
@ -344,9 +347,10 @@ class Autosummary(SphinxDirective):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes)
|
real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes)
|
||||||
except ImportError:
|
except ImportExceptionGroup as exc:
|
||||||
logger.warning(__('autosummary: failed to import %s'), name,
|
errors = list(set("* %s: %s" % (type(e).__name__, e) for e in exc.exceptions))
|
||||||
location=self.get_location())
|
logger.warning(__('autosummary: failed to import %s.\nPossible hints:\n%s'),
|
||||||
|
name, '\n'.join(errors), location=self.get_location())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.bridge.result = StringList() # initialize for each documenter
|
self.bridge.result = StringList() # initialize for each documenter
|
||||||
@ -620,6 +624,18 @@ def limited_join(sep: str, items: List[str], max_chars: int = 30,
|
|||||||
|
|
||||||
# -- Importing items -----------------------------------------------------------
|
# -- 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]:
|
def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Obtain current Python import prefixes (for `import_by_name`)
|
Obtain current Python import prefixes (for `import_by_name`)
|
||||||
@ -641,26 +657,38 @@ def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
|
|||||||
return prefixes
|
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
|
"""Import a Python object that has the given *name*, under one of the
|
||||||
*prefixes*. The first name that succeeds is used.
|
*prefixes*. The first name that succeeds is used.
|
||||||
"""
|
"""
|
||||||
tried = []
|
tried = []
|
||||||
|
errors: List[ImportExceptionGroup] = []
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
try:
|
try:
|
||||||
if prefix:
|
if prefix:
|
||||||
prefixed_name = '.'.join([prefix, name])
|
prefixed_name = '.'.join([prefix, name])
|
||||||
else:
|
else:
|
||||||
prefixed_name = name
|
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
|
return prefixed_name, obj, parent, modname
|
||||||
except ImportError:
|
except ImportError:
|
||||||
tried.append(prefixed_name)
|
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."""
|
"""Import a Python object given its full name."""
|
||||||
|
errors: List[BaseException] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
name_parts = name.split('.')
|
name_parts = name.split('.')
|
||||||
|
|
||||||
@ -670,8 +698,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
|
|||||||
try:
|
try:
|
||||||
mod = import_module(modname)
|
mod = import_module(modname)
|
||||||
return getattr(mod, name_parts[-1]), mod, modname
|
return getattr(mod, name_parts[-1]), mod, modname
|
||||||
except (ImportError, IndexError, AttributeError):
|
except (ImportError, IndexError, AttributeError) as exc:
|
||||||
pass
|
errors.append(exc.__cause__ or exc)
|
||||||
|
|
||||||
# ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
|
# ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
|
||||||
last_j = 0
|
last_j = 0
|
||||||
@ -681,8 +709,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
|
|||||||
modname = '.'.join(name_parts[:j])
|
modname = '.'.join(name_parts[:j])
|
||||||
try:
|
try:
|
||||||
import_module(modname)
|
import_module(modname)
|
||||||
except ImportError:
|
except ImportError as exc:
|
||||||
continue
|
errors.append(exc.__cause__ or exc)
|
||||||
|
|
||||||
if modname in sys.modules:
|
if modname in sys.modules:
|
||||||
break
|
break
|
||||||
@ -696,25 +724,32 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
|
|||||||
return obj, parent, modname
|
return obj, parent, modname
|
||||||
else:
|
else:
|
||||||
return sys.modules[modname], None, modname
|
return sys.modules[modname], None, modname
|
||||||
except (ValueError, ImportError, AttributeError, KeyError) as e:
|
except (ValueError, ImportError, AttributeError, KeyError) as exc:
|
||||||
raise ImportError(*e.args) from e
|
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
|
"""Import an instance variable that has the given *name*, under one of the
|
||||||
*prefixes*. The first name that succeeds is used.
|
*prefixes*. The first name that succeeds is used.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
name, attr = name.rsplit(".", 1)
|
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 + ".", "")
|
qualname = real_name.replace(modname + ".", "")
|
||||||
analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname))
|
analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname))
|
||||||
analyzer.analyze()
|
analyzer.analyze()
|
||||||
# check for presence in `annotations` to include dataclass attributes
|
# check for presence in `annotations` to include dataclass attributes
|
||||||
if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations:
|
if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations:
|
||||||
return real_name + "." + attr, INSTANCEATTR, obj, modname
|
return real_name + "." + attr, INSTANCEATTR, obj, modname
|
||||||
except (ImportError, ValueError, PycodeError):
|
except (ImportError, ValueError, PycodeError) as exc:
|
||||||
pass
|
raise ImportError from exc
|
||||||
|
except ImportExceptionGroup:
|
||||||
|
raise # pass through it as is
|
||||||
|
|
||||||
raise ImportError
|
raise ImportError
|
||||||
|
|
||||||
@ -739,8 +774,8 @@ class AutoLink(SphinxRole):
|
|||||||
try:
|
try:
|
||||||
# try to import object by name
|
# try to import object by name
|
||||||
prefixes = get_import_prefixes_from_env(self.env)
|
prefixes = get_import_prefixes_from_env(self.env)
|
||||||
import_by_name(pending_xref['reftarget'], prefixes)
|
import_by_name(pending_xref['reftarget'], prefixes, grouped_exception=True)
|
||||||
except ImportError:
|
except ImportExceptionGroup:
|
||||||
literal = cast(nodes.literal, pending_xref[0])
|
literal = cast(nodes.literal, pending_xref[0])
|
||||||
objects[0] = nodes.emphasis(self.rawtext, literal.astext(),
|
objects[0] = nodes.emphasis(self.rawtext, literal.astext(),
|
||||||
classes=literal['classes'])
|
classes=literal['classes'])
|
||||||
|
@ -41,7 +41,8 @@ from sphinx.config import Config
|
|||||||
from sphinx.deprecation import RemovedInSphinx50Warning
|
from sphinx.deprecation import RemovedInSphinx50Warning
|
||||||
from sphinx.ext.autodoc import Documenter
|
from sphinx.ext.autodoc import Documenter
|
||||||
from sphinx.ext.autodoc.importer import import_module
|
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.locale import __
|
||||||
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||||
from sphinx.registry import SphinxComponentRegistry
|
from sphinx.registry import SphinxComponentRegistry
|
||||||
@ -430,15 +431,22 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
|
|||||||
ensuredir(path)
|
ensuredir(path)
|
||||||
|
|
||||||
try:
|
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 + ".", "")
|
qualname = name.replace(modname + ".", "")
|
||||||
except ImportError as e:
|
except ImportExceptionGroup as exc:
|
||||||
try:
|
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)
|
name, obj, parent, modname = import_ivar_by_name(entry.name)
|
||||||
qualname = name.replace(modname + ".", "")
|
qualname = name.replace(modname + ".", "")
|
||||||
except ImportError:
|
except ImportError as exc2:
|
||||||
logger.warning(__('[autosummary] failed to import %r: %s') % (entry.name, e))
|
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
|
continue
|
||||||
|
|
||||||
context: Dict[str, Any] = {}
|
context: Dict[str, Any] = {}
|
||||||
@ -500,13 +508,14 @@ def find_autosummary_in_docstring(name: str, module: str = None, filename: str =
|
|||||||
RemovedInSphinx50Warning, stacklevel=2)
|
RemovedInSphinx50Warning, stacklevel=2)
|
||||||
|
|
||||||
try:
|
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()
|
lines = pydoc.getdoc(obj).splitlines()
|
||||||
return find_autosummary_in_lines(lines, module=name, filename=filename)
|
return find_autosummary_in_lines(lines, module=name, filename=filename)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
except ImportError as e:
|
except ImportExceptionGroup as exc:
|
||||||
print("Failed to import '%s': %s" % (name, e))
|
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:
|
except SystemExit:
|
||||||
print("Failed to import '%s'; the module executes module level "
|
print("Failed to import '%s'; the module executes module level "
|
||||||
"statement and it might call sys.exit()." % name)
|
"statement and it might call sys.exit()." % name)
|
||||||
|
Loading…
Reference in New Issue
Block a user