import ast
from typing import TYPE_CHECKING, Tuple, Optional, cast
from pyfileconf.exceptions.imports import NoImportStatementException
if TYPE_CHECKING:
from pyfileconf.views.object import ObjectView
from importlib.util import resolve_name
from pyfileconf.imports.models.statements.interfaces import (
ObjectImportStatement,
ModuleImportStatement,
AnyImportStatement
)
from pyfileconf.io.file.load.parsers.funcdef import extract_function_definition_or_class_init_from_ast_by_name
from pyfileconf.io.file.load.lazy.base.impassign import ImportAssignmentLazyLoader
from pyfileconf.imports.models.statements.container import ImportStatementContainer
ArgumentsAndImports = Tuple[ast.arguments, ImportStatementContainer]
[docs]def extract_function_args_and_arg_imports_from_import(function_name: str, imp: Optional[AnyImportStatement],
import_section_path_str: Optional[str] = None) -> ArgumentsAndImports:
from pyfileconf.io.func.load.extractimp import extract_import_statements_from_function_args_imports_and_assigns
if imp is None:
raise NoImportStatementException('no import passed to extract function and imports')
filepath = get_module_filepath_from_import(
imp,
import_section_path_str
)
if function_name in imp.renames.new_names:
# If was renamed in the import,
# Get original name from rename statement
function_name = imp.renames.reverse_name_map[function_name]
loader = ImportAssignmentLazyLoader(filepath)
# TODO [#44]: handle relative nested imports such as from .this import that
#
# This code keeps importing and checking whether the name is defined in the file. If not,
# it follows the next import. This doesn't properly work if one of those followed imports
# is a relative import such as `from .this import that`. Add test to show this and
# also fix it.
ast_function_def = extract_function_definition_or_class_init_from_ast_by_name(
loader.ast,
function_name
)
if ast_function_def is not None:
# Found function definition in this import
# TODO [#12]: handle other class methods - also update extract_function_definition function
if ast_function_def.name == '__init__':
# Got class init
del ast_function_def.args.args[0] # delete first arg (self)
function_arg_imports = extract_import_statements_from_function_args_imports_and_assigns(
ast_function_def.args,
loader.imports,
loader.assigns,
imp.module
)
return ast_function_def.args, function_arg_imports
# Else, this function must have been imported into this file as well.
# Must find the matching import, the extract args from that import
next_level_import = loader.imports.get_import_for_module_or_obj_name(function_name)
if next_level_import is None:
raise ValueError(f'not able to find function definition for {function_name}. Could not '
f'find in definitions or in imports')
if import_section_path_str is not None:
next_module = next_level_import.module
new_section_path = resolve_name(next_module, import_section_path_str) if next_module.startswith('.') else next_module
else:
new_section_path = None
return extract_function_args_and_arg_imports_from_import(
function_name,
imp=next_level_import,
import_section_path_str=new_section_path
)
[docs]def get_module_filepath_from_import(imp: Optional[AnyImportStatement], import_section_path_str: Optional[str]):
if imp is None:
return None
# TODO [#30]: figure out why mypy won't pick up that imp is not None in get_module_filepath_from_import
#
# Then type ignores can be removed
if isinstance(imp, ObjectImportStatement):
func = lambda: imp.get_module_filepath(import_section_path_str)
elif isinstance(imp, ModuleImportStatement):
# have already ensured in creating ObjectView that there should be only one module
# in the module import statement. So just take the first filepath
func = lambda: imp.get_module_filepaths(import_section_path_str)[imp.modules[0]] # type: ignore
else:
raise ValueError(f'expected import statement to be ObjectImportStatement or ModuleImportStatement.'
f' Got {imp} of type {type(imp)}')
return func()