diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 379fd48ad0..8810a5088f 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -614,9 +614,11 @@ 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], last_errors = None) -> 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. + If importing fails, *last_errors* will contain the exceptions raised during + import and may help to identify the problem. """ tried = [] for prefix in prefixes: @@ -625,14 +627,14 @@ def import_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, A 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, last_errors) return prefixed_name, obj, parent, modname except ImportError: tried.append(prefixed_name) 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, last_errors = None) -> Tuple[Any, Any, str]: """Import a Python object given its full name.""" try: name_parts = name.split('.') @@ -643,8 +645,9 @@ 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 e: + if last_errors is not None: + last_errors.append(e) # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ... last_j = 0 @@ -654,7 +657,9 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]: modname = '.'.join(name_parts[:j]) try: import_module(modname) - except ImportError: + except ImportError as e: + if last_errors is not None: + last_errors.append(e) continue if modname in sys.modules: @@ -673,7 +678,7 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]: raise ImportError(*e.args) from e -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], last_errors = None) -> 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. """ @@ -686,8 +691,9 @@ def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, A # 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 e: + if last_errors is not None: + last_errors.append(e) raise ImportError diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 4f3493659d..dcbebd341d 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -413,16 +413,21 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, path = output_dir or os.path.abspath(entry.path) ensuredir(path) + last_errors = [] + try: - name, obj, parent, modname = import_by_name(entry.name) + name, obj, parent, modname = import_by_name(entry.name, last_errors=last_errors) qualname = name.replace(modname + ".", "") except ImportError as e: try: - # try to importl as an instance attribute - name, obj, parent, modname = import_ivar_by_name(entry.name) + # try to import as an instance attribute + name, obj, parent, modname = import_ivar_by_name(entry.name, last_errors=last_errors) qualname = name.replace(modname + ".", "") except ImportError: - logger.warning(__('[autosummary] failed to import %r: %s') % (entry.name, e)) + # Convert exceptions to strings and remove duplicates (convert to set, then to list) + last_errors_str = list(set(str(e) for e in last_errors)) + logger.warning(__('[autosummary] failed to import %r: %s. Possible hints: %s') % + (entry.name, e, last_errors_str)) continue context: Dict[str, Any] = {}