from typing import List, cast
import ast
import warnings
from pyfileconf.io.func.load.config import function_args_as_arg_and_annotation_dict
from pyfileconf.io.file.load.parsers.extname import extract_unique_external_names_from_assign_value
from pyfileconf.imports.models.statements.container import ImportStatementContainer, ObjectImportStatement
from pyfileconf.assignments.models.container import AssignmentStatementContainer
from pyfileconf.logic.inspect import _is_str_matching_builtin_type
[docs]def extract_import_statements_from_function_args_imports_and_assigns(args: ast.arguments,
imports: ImportStatementContainer,
assigns: AssignmentStatementContainer,
current_module_section_path_str: str,
) -> ImportStatementContainer:
defaults_dict, annotation_dict = function_args_as_arg_and_annotation_dict(args)
# Extract external names. These are the root names (not attrs, the base of attrs) for anything that is not builtin
external_names = _unique_external_names_from_default_dict_and_annotation_dict(defaults_dict, annotation_dict)
out_imports = ImportStatementContainer([])
# External names may be either due to assignment or due to imports
# Handle names due to assignment
for name in external_names:
if assigns.contains_varname(name):
# External name is due to assignment in this module. Create import from this module
out_imports.append(ObjectImportStatement.from_str(f'from {current_module_section_path_str} import {name}'))
# Handle names which are imported into this file
for name in external_names:
import_or_none = imports.get_import_for_module_or_obj_name(name)
if import_or_none is not None:
out_imports.append(import_or_none)
# Sanity check, did we find all the imports?
n_external = len(external_names)
n_imports = len(out_imports)
if n_external != n_imports:
warnings.warn(f'had {n_external} external names from function definition, only '
f'found {n_imports} imports. May be missing imports')
return out_imports
def _unique_external_names_from_default_dict_and_annotation_dict(default_dict: dict,
annotation_dict: dict) -> List[str]:
names = _unique_external_names_from_assignment_dict(default_dict)
names += _unique_external_names_from_annotation_dict(annotation_dict)
return list(set(names))
def _unique_external_names_from_multiple_assignment_dicts(*assignment_dicts) -> List[str]:
all_names: List[str] = []
for assignment_dict in assignment_dicts:
all_names += _unique_external_names_from_assignment_dict(assignment_dict)
return list(set(all_names))
def _unique_external_names_from_assignment_dict(assignment_dict: dict) -> List[str]:
names = []
for key, value in assignment_dict.items():
new_names = extract_unique_external_names_from_assign_value(value)
names.extend(new_names)
no_none_names: List[str] = [name for name in names if name is not None]
return list(set(no_none_names))
def _unique_external_names_from_annotation_dict(annotation_dict: dict) -> List[str]:
type_str_names = _extract_unique_type_str_names_from_annotation_dict(annotation_dict)
return [type_str for type_str in type_str_names if not _is_str_matching_builtin_type(type_str)]
def _extract_unique_type_str_names_from_annotation_dict(annotation_dict: dict) -> List[str]:
names = []
for value in annotation_dict.values():
names.extend(_extract_str_names_from_ambiguous_annotation(value))
return list(set(names))
def _extract_str_names_from_ambiguous_annotation(annotation) -> List[str]:
# TODO [#26]: remove type ignores once typeshed has better ast support
#
# Hitting errors "expr" has no attribute "id" and "slice" has no attribute "value"
names = []
if isinstance(annotation, ast.Name):
names.append(annotation.id) # handles most types
elif hasattr(annotation, 'elts'): # type: ignore
# got multiple values, e.g. List[str, bool]
# in this case, value.slice.value is a Tuple, and Tuple.elts contains the items
names.extend(_extract_str_names_from_tuple(annotation)) # type: ignore
elif isinstance(annotation, ast.Index):
# e.g. [str]
# use .value to extract the str part, but then as it can be anything, call recursively on this
names.extend(_extract_str_names_from_ambiguous_annotation(annotation.value))
elif isinstance(annotation, ast.Subscript):
# E.g. Tuple[int]
names.extend(_extract_str_names_from_subscript(annotation))
elif isinstance(annotation, ast.Attribute):
# E.g. pd.DataFrame
names.append(annotation.value.id) # type: ignore
elif isinstance(annotation, ast.Str):
# E.g. 'DataPipeline' (quoted type declaration where type was imported if TYPE_CHECKING)
names.append(annotation.s)
else:
raise NotImplementedError(f'no handling for {annotation} of type {type(annotation)}')
return names
def _extract_str_names_from_subscript(subscript: ast.Subscript) -> List[str]:
names = []
# for example, List[str]
outer_ast = subscript.value # List portion
inner_ast = subscript.slice # [str] portion
names.extend(_extract_str_names_from_ambiguous_annotation(outer_ast))
names.extend(_extract_str_names_from_ambiguous_annotation(inner_ast))
return names
def _extract_str_names_from_tuple(tuple: ast.Tuple) -> List[str]:
names = []
for item in tuple.elts:
names.extend(_extract_str_names_from_ambiguous_annotation(item))
return names