Source code for polymerist.genutils.importutils.pyimports
'''For inspecting and managing toplevel imports within Python files and modules'''
__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'
from typing import Optional
from types import ModuleType
from dataclasses import dataclass, field
import ast
from pathlib import Path
from .pkginspect import is_module, is_package
from ..fileutils.pathutils import allow_string_paths
[docs]
@dataclass
class ImportedObjectInfo:
'''For encapsulating info about an object imported in a Python file'''
object_name : str
object_alias : Optional[str] = field(default=None)
parent_module : Optional[str] = field(default=None)
source_file : Optional[Path] = field(default=None)
line_number : Optional[int] = field(default=None)
is_relative : Optional[bool] = field(default=None)
[docs]
@allow_string_paths
def extract_imports_from_pyfile(pyfile_path : Path) -> list[ImportedObjectInfo]:
'''Compiles info from all Python imports in a Python (.py) file'''
if not pyfile_path.is_file():
raise ValueError('Cannot interpret non-file path as file')
with pyfile_path.open('r') as pyfile:
module_root = ast.parse(pyfile.read(), pyfile_path)
import_info : list[ImportedObjectInfo] = []
for syntax_node in module_root.body:
if not isinstance(syntax_node, (ast.Import, ast.ImportFrom)):
continue
if isinstance(syntax_node, ast.ImportFrom):
parent_module : Optional[str] = syntax_node.module
is_relative : bool = (syntax_node.level != 0)
else: # implcictly, this MUST be an ast.Import instance by the initial exclusion check
parent_module : Optional[str] = None
is_relative : bool = False
for imported_object in syntax_node.names:
import_info.append(
ImportedObjectInfo(
object_name=imported_object.name,
object_alias=imported_object.asname,
parent_module=parent_module,
source_file=pyfile_path,
line_number=syntax_node.lineno, # .end_lineno
is_relative=is_relative,
)
)
return import_info
[docs]
@allow_string_paths
def extract_imports_from_dir(source_dir : Path) -> list[ImportedObjectInfo]:
'''Compiles info from all Python imports in any and all Python files in a directory'''
if not source_dir.is_dir():
raise ValueError('Cannot interpret non-directory path as directory')
import_infos = []
for pyfile_path in source_dir.glob('**/*.py'):
import_infos.extend(extract_imports_from_pyfile(pyfile_path))
return import_infos
[docs]
def extract_imports_from_module(module : ModuleType) -> list[ImportedObjectInfo]:
'''Compiles info from all Python imports in a Python (.py) file'''
if is_package(module):
return extract_imports_from_dir(module.__path__[0]) # TODO: provide package-specific, non-recursive implementation of this
else: # all packages a re modules, but not all modules a re packages; hence, the check must be done in this order
return extract_imports_from_pyfile(module.__file__)