"""Utility functions and types."""from__future__importannotationsimportenumfromfnmatchimportfnmatchimportosfrompathlibimportPathimporttypingastfromtyping_extensionsimportRequiredPROPERTY_TYPE=t.Literal["async","staticmethod","classmethod","abstractmethod","singledispatch"]ARGS_TYPE=t.List[t.Tuple[t.Optional[str],t.Optional[str],t.Optional[str],t.Optional[str]]]
[docs]classItemData(t.TypedDict,total=False):"""A data item, for the results of the analysis."""type:Required[str]full_name:Required[str]# this is delimited by dotsdoc:Required[str]range:tuple[int,int]## module / packagefile_path:None|strencoding:strall:None|list[str]imports:list[tuple[str,str|None]]# path, alias# assign (data)value:None|str|t.Any# TODO make value JSON serializableannotation:None|str# function/method/overloadproperties:list[PROPERTY_TYPE]args:ARGS_TYPEreturn_annotation:None|str# classbases:list[str]# class or methoddoc_inherited:str# child of classinherited:str
[docs]classWarningSubtypes(enum.Enum):"""The subtypes of warnings for the extension."""CONFIG_ERROR="config_error""""Issue with configuration validation."""GIT_CLONE_FAILED="git_clone""""Failed to clone a git repository."""MISSING_MODULE="missing_module""""If the package file/folder does not exist."""DUPLICATE_ITEM="dup_item""""Duplicate fully qualified name found during package analysis."""RENDER_ERROR="render""""Generic rendering error."""ALL_MISSING="all_missing""""__all__ attribute missing or empty in a module."""ALL_RESOLUTION="all_resolve""""Issue with resolution of an item in a module's __all__ attribute."""NAME_NOT_FOUND="missing"
[docs]defyield_modules(folder:str|Path,*,root_module:str|None=None,extensions:t.Sequence[str]=(".py",".pyi"),exclude_dirs:t.Sequence[str]=("__pycache__",),exclude_files:t.Sequence[str]=(),)->t.Iterable[tuple[Path,str]]:"""Walk the given folder and yield all required modules. :param folder: The path to walk. :param root_module: The name of the root module, otherwise the folder name is used. :param extensions: The extensions to include. If multiple files with the same stem, only the first extension will be used. :param exclude_dirs: Directory names to exclude (matched with fnmatch). :param exclude_files: File names to exclude (matched with fnmatch). """folder=Path(folder)root_mod=(root_moduleorfolder.name).split(".")exc_dirs=set(exclude_dirsor[])exc_files=set(exclude_filesor[])def_suffix_sort_key(s:str)->int:returnextensions.index(s)forroot,dirs,filenamesinos.walk(folder,topdown=True):dirs[:]=[dfordindirsifnotany(fnmatch(d,m)forminexc_dirs)]to_yield:dict[str,list[str]]={}forfilenameinfilenames:ifany(fnmatch(filename,m)forminexc_files):continuename,suffix=os.path.splitext(filename)ifsuffixinextensions:to_yield.setdefault(name,[]).append(suffix)root_path=Path(root)rel_mod=root_path.relative_to(folder).partsforname,suffixesinto_yield.items():suffix=sorted(suffixes,key=_suffix_sort_key)[0]yield(root_path/f"{name}{suffix}",".".join([*root_mod,*rel_mod,name]))