"""Handling of ``__all__`` resolution."""from__future__importannotationsimporttypingastfromautodoc2.dbimportDatabase
[docs]classAllResolutionError(Exception):"""An error occurred while resolving the ``__all__``."""
[docs]classObjectMissingError(AllResolutionError):"""An object in the ``__all__`` is not available in the database."""
[docs]classCircularImportError(AllResolutionError):"""A circular import was detected."""
[docs]classNoAllError(AllResolutionError):"""The module does not have an ``__all__``."""
[docs]classAllResolveResult(t.TypedDict):resolved:dict[str,str]"""Resolved is a dict of ``{full_name: {name}}``"""errors:list[tuple[str,str]]"""Errors are tuples of ``(full_name, error_message)``"""
[docs]classAllResolver:def__init__(self,db:Database,warn_func:t.Callable[[str],None]|None=None)->None:"""Initialise the resolver. :param db: the database to use :param warn_func: a function to call with warnings """self._db=dbself._warn_func=(lambdax:None)ifwarn_funcisNoneelsewarn_funcself._resolve_cache:dict[str,AllResolveResult]={}
[docs]defclear_cache(self)->None:"""Clear the cache."""self._resolve_cache={}
[docs]defget_resolved_all(self,full_name:str,_breadcrumbs:tuple[str,...]=())->AllResolveResult:"""Yield all names that would be imported by star. :param full_name: the fully qualified name of the module :param _breadcrumbs: used to detect circular imports """iffull_nameinself._resolve_cache:returnself._resolve_cache[full_name]iffull_nameinself._resolve_cache:returnself._resolve_cache[full_name]iffull_namein_breadcrumbs:raiseCircularImportError(f"Circular import detected: {full_name} -> {_breadcrumbs}")if(item:=self._db.get_item(full_name))isNone:raiseObjectMissingError(full_name)if(all_list:=item.get("all"))isNone:raiseNoAllError(f"{full_name!r} does not have an __all__")resolved:dict[str,set[str]]={name:set()fornameinall_list}errors:list[tuple[str,str]]=[]# direct childrenfornameinall_list:iff"{full_name}.{name}"inself._db:resolved[name].add(f"{full_name}.{name}")# direct importsstar_imports:list[str]=[]forimport_name,aliasinitem.get("imports",[]):final_name=aliasorimport_name.split(".")[-1]iffinal_name=="*":star_imports.append(import_name[:-2])eliffinal_nameinresolved:resolved[final_name].add(import_name)# star importsforimport_nameinstar_imports:# TODO how do we "bubble up" errors? so that we can report themtry:resolved_import=self.get_resolved_all(import_name,(*_breadcrumbs,full_name))exceptObjectMissingError:errors.append((full_name,f"from {import_name} import *; is unknown"))exceptCircularImportError:errors.append((full_name,f"from {import_name} import *; is a circular import"))exceptNoAllError:errors.append((full_name,f"from {import_name} import *; does not have an __all__",))else:errors.extend(resolved_import["errors"])forname,itemsinresolved_import["resolved"].items():ifnameinresolved:resolved[name].add(items)final_resolved:dict[str,str]={}forname,rnamesinresolved.items():ifnotrnames:errors.append((full_name,f"{name!r} is unknown"))eliflen(rnames)>1:errors.append((full_name,f"{name!r} is ambiguous: {rnames}"))else:final_resolved[name]=rnames.pop()forerrorinerrors:self._warn_func(f"{full_name}: {error[0]}: {error[1]}")self._resolve_cache[full_name]={"resolved":final_resolved,"errors":errors,}returnself._resolve_cache[full_name]
[docs]defget_name(self,name:str)->str|None:"""Get the item, first by trying the fully qualified name, then by looking at __all__ in parent modules. """ifnameinself._db:returnnameparts=name.split(".")iflen(parts)<2:returnNoneparent,child=".".join(parts[:-1]),parts[-1]try:resolved=self.get_resolved_all(parent)exceptAllResolutionError:returnNonereturnresolved["resolved"].get(child,None)