6161from inspect import Parameter
6262from os import path
6363from types import ModuleType
64- from typing import Any , Dict , List , Optional , Tuple , Type , cast
64+ from typing import Any , Dict , List , Optional , Sequence , Tuple , Type , cast
6565
6666from docutils import nodes
6767from docutils .nodes import Element , Node , system_message
@@ -306,15 +306,18 @@ def run(self) -> List[Node]:
306306 def import_by_name (self , name : str , prefixes : List [str ]) -> Tuple [str , Any , Any , str ]:
307307 with mock (self .config .autosummary_mock_imports ):
308308 try :
309- return import_by_name (name , prefixes )
310- except ImportError as exc :
309+ return import_by_name (name , prefixes , grouped_exception = True )
310+ except ImportExceptionGroup as exc :
311311 # check existence of instance attribute
312312 try :
313313 return import_ivar_by_name (name , prefixes )
314- except ImportError :
315- pass
314+ except ImportError as exc2 :
315+ if exc2 .__cause__ :
316+ errors : List [BaseException ] = exc .exceptions + [exc2 .__cause__ ]
317+ else :
318+ errors = exc .exceptions + [exc2 ]
316319
317- raise exc # re- raise ImportError if instance attribute not found
320+ raise ImportExceptionGroup ( exc . args [ 0 ], errors )
318321
319322 def create_documenter (self , app : Sphinx , obj : Any ,
320323 parent : Any , full_name : str ) -> "Documenter" :
@@ -344,9 +347,10 @@ def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]:
344347
345348 try :
346349 real_name , obj , parent , modname = self .import_by_name (name , prefixes = prefixes )
347- except ImportError :
348- logger .warning (__ ('autosummary: failed to import %s' ), name ,
349- location = self .get_location ())
350+ except ImportExceptionGroup as exc :
351+ errors = list (set ("* %s: %s" % (type (e ).__name__ , e ) for e in exc .exceptions ))
352+ logger .warning (__ ('autosummary: failed to import %s.\n Possible hints:\n %s' ),
353+ name , '\n ' .join (errors ), location = self .get_location ())
350354 continue
351355
352356 self .bridge .result = StringList () # initialize for each documenter
@@ -620,6 +624,18 @@ def limited_join(sep: str, items: List[str], max_chars: int = 30,
620624
621625# -- Importing items -----------------------------------------------------------
622626
627+
628+ class ImportExceptionGroup (Exception ):
629+ """Exceptions raised during importing the target objects.
630+
631+ It contains an error messages and a list of exceptions as its arguments.
632+ """
633+
634+ def __init__ (self , message : Optional [str ], exceptions : Sequence [BaseException ]):
635+ super ().__init__ (message )
636+ self .exceptions = list (exceptions )
637+
638+
623639def get_import_prefixes_from_env (env : BuildEnvironment ) -> List [str ]:
624640 """
625641 Obtain current Python import prefixes (for `import_by_name`)
@@ -641,26 +657,38 @@ def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]:
641657 return prefixes
642658
643659
644- def import_by_name (name : str , prefixes : List [str ] = [None ]) -> Tuple [str , Any , Any , str ]:
660+ def import_by_name (name : str , prefixes : List [str ] = [None ], grouped_exception : bool = False
661+ ) -> Tuple [str , Any , Any , str ]:
645662 """Import a Python object that has the given *name*, under one of the
646663 *prefixes*. The first name that succeeds is used.
647664 """
648665 tried = []
666+ errors : List [ImportExceptionGroup ] = []
649667 for prefix in prefixes :
650668 try :
651669 if prefix :
652670 prefixed_name = '.' .join ([prefix , name ])
653671 else :
654672 prefixed_name = name
655- obj , parent , modname = _import_by_name (prefixed_name )
673+ obj , parent , modname = _import_by_name (prefixed_name , grouped_exception )
656674 return prefixed_name , obj , parent , modname
657675 except ImportError :
658676 tried .append (prefixed_name )
659- raise ImportError ('no module named %s' % ' or ' .join (tried ))
677+ except ImportExceptionGroup as exc :
678+ tried .append (prefixed_name )
679+ errors .append (exc )
680+
681+ if grouped_exception :
682+ exceptions : List [BaseException ] = sum ((e .exceptions for e in errors ), [])
683+ raise ImportExceptionGroup ('no module named %s' % ' or ' .join (tried ), exceptions )
684+ else :
685+ raise ImportError ('no module named %s' % ' or ' .join (tried ))
660686
661687
662- def _import_by_name (name : str ) -> Tuple [Any , Any , str ]:
688+ def _import_by_name (name : str , grouped_exception : bool = False ) -> Tuple [Any , Any , str ]:
663689 """Import a Python object given its full name."""
690+ errors : List [BaseException ] = []
691+
664692 try :
665693 name_parts = name .split ('.' )
666694
@@ -670,8 +698,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
670698 try :
671699 mod = import_module (modname )
672700 return getattr (mod , name_parts [- 1 ]), mod , modname
673- except (ImportError , IndexError , AttributeError ):
674- pass
701+ except (ImportError , IndexError , AttributeError ) as exc :
702+ errors . append ( exc . __cause__ or exc )
675703
676704 # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
677705 last_j = 0
@@ -681,8 +709,8 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
681709 modname = '.' .join (name_parts [:j ])
682710 try :
683711 import_module (modname )
684- except ImportError :
685- continue
712+ except ImportError as exc :
713+ errors . append ( exc . __cause__ or exc )
686714
687715 if modname in sys .modules :
688716 break
@@ -696,25 +724,32 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]:
696724 return obj , parent , modname
697725 else :
698726 return sys .modules [modname ], None , modname
699- except (ValueError , ImportError , AttributeError , KeyError ) as e :
700- raise ImportError (* e .args ) from e
727+ except (ValueError , ImportError , AttributeError , KeyError ) as exc :
728+ errors .append (exc )
729+ if grouped_exception :
730+ raise ImportExceptionGroup ('' , errors )
731+ else :
732+ raise ImportError (* exc .args ) from exc
701733
702734
703- def import_ivar_by_name (name : str , prefixes : List [str ] = [None ]) -> Tuple [str , Any , Any , str ]:
735+ def import_ivar_by_name (name : str , prefixes : List [str ] = [None ],
736+ grouped_exception : bool = False ) -> Tuple [str , Any , Any , str ]:
704737 """Import an instance variable that has the given *name*, under one of the
705738 *prefixes*. The first name that succeeds is used.
706739 """
707740 try :
708741 name , attr = name .rsplit ("." , 1 )
709- real_name , obj , parent , modname = import_by_name (name , prefixes )
742+ real_name , obj , parent , modname = import_by_name (name , prefixes , grouped_exception )
710743 qualname = real_name .replace (modname + "." , "" )
711744 analyzer = ModuleAnalyzer .for_module (getattr (obj , '__module__' , modname ))
712745 analyzer .analyze ()
713746 # check for presence in `annotations` to include dataclass attributes
714747 if (qualname , attr ) in analyzer .attr_docs or (qualname , attr ) in analyzer .annotations :
715748 return real_name + "." + attr , INSTANCEATTR , obj , modname
716- except (ImportError , ValueError , PycodeError ):
717- pass
749+ except (ImportError , ValueError , PycodeError ) as exc :
750+ raise ImportError from exc
751+ except ImportExceptionGroup :
752+ raise # pass through it as is
718753
719754 raise ImportError
720755
@@ -739,8 +774,8 @@ def run(self) -> Tuple[List[Node], List[system_message]]:
739774 try :
740775 # try to import object by name
741776 prefixes = get_import_prefixes_from_env (self .env )
742- import_by_name (pending_xref ['reftarget' ], prefixes )
743- except ImportError :
777+ import_by_name (pending_xref ['reftarget' ], prefixes , grouped_exception = True )
778+ except ImportExceptionGroup :
744779 literal = cast (nodes .literal , pending_xref [0 ])
745780 objects [0 ] = nodes .emphasis (self .rawtext , literal .astext (),
746781 classes = literal ['classes' ])
0 commit comments