Last active
March 16, 2021 12:44
-
-
Save ntaraujo/f9454487dc69fcd81ba31178772d1434 to your computer and use it in GitHub Desktop.
import module/object referenced as string/path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from importlib import import_module | |
from importlib.util import spec_from_file_location, module_from_spec | |
from sys import modules | |
from os.path import dirname, abspath, isfile | |
from re import compile | |
from os import PathLike | |
from types import ModuleType | |
from typing import Union, Any, TypeVar, List, Optional | |
_string_type = Union[str, bytes] | |
_path_type = Union[_string_type, PathLike] | |
_module_type = TypeVar('_module_type', ModuleType, Any) | |
def module(name: str, obj=False, from_path: _path_type = None, cache=True) -> _module_type: | |
""" | |
Import a module/object searching for its name or path and return it. | |
:param name: module's (object) name (mandatory even with 'from_path') | |
:param obj: if 'name' refers to an object | |
:param from_path: the module's path if needed | |
:param cache: if the existent reference in system should be kept/used | |
:raise ValueError: obj == True but 'name' refers to a module | |
:raise ModuleNotFoundError: | |
:raise FileNotFoundError: | |
:raise AttributeError: obj == True but the object reference do not exist in module | |
:raise Exception: any internal module error | |
Import a module: | |
>>> o_path = module('os.path') | |
Import a module's object: | |
>>> p_join = module('os.path.join', obj=True) | |
Import a module from a path: | |
>>> my_module = module('my_package.my_module', from_path='my_dir/my_package/my_module.py') | |
Import a module's object from a path: | |
>>> my_obj = module('my_module.my_object', obj=True, from_path='my_dir/my_module.py') | |
Import a module again even if its already imported: | |
>>> o_path = module('os.path', cache=False) | |
""" | |
if obj: | |
name, var = name.rsplit('.', 1) | |
if not cache and name in modules: | |
del modules[name] | |
if from_path is None: | |
m = import_module(name) | |
else: | |
from_path = abspath(from_path) | |
if (m := module_from_file(name, from_path)) is None: | |
raise ModuleNotFoundError(f"No module in '{from_path}'") | |
if obj: | |
return getattr(m, var) # noqa | |
else: | |
return m | |
def import_eval(source: _path_type, scope: dict, encoding=None): | |
""" | |
Evaluate import expressions in given scope. | |
:param source: source code as text or path to source code | |
:param scope: a dict e.g. globals(), vars(), __dict__ | |
:param encoding: if source is at bytes or for the file | |
:return: a list with information about failed imports | |
>>> import_eval('from os.path import join as p_join', vars()) | |
""" | |
if encoding is not None and isinstance(source, bytes): | |
source = source.decode(encoding=encoding) | |
lines = source.splitlines() | |
fails = [] | |
if len(lines) == 1 and isfile(file := lines[0]): | |
if encoding is None: | |
lines = open(file, 'r').readlines() # noqa | |
else: | |
lines = open(file, 'r', encoding=encoding).readlines() # noqa | |
for line in lines: | |
if 'import ' not in line: | |
continue | |
level1 = _import_sub('', _from_import_sub(r'\1.', line.lstrip())) | |
if matches := _as_match(level1): | |
name, alias = matches.group(1), matches.group(2) | |
try: | |
scope[alias] = auto_module(name) | |
except (ModuleNotFoundError, AttributeError) as e: | |
fails.append({"line": line, "name": name, "alias": alias, "exception": e}) | |
else: | |
level2 = level1.split(',') | |
parent = level1.rsplit('.', 1)[0] | |
for i, ref in enumerate(level2): | |
if i: | |
alias = ref.rsplit('.', 1)[-1] | |
name = f"{parent}.{alias}" | |
else: | |
alias = ref.lstrip() | |
name = f"{parent}.{alias}" | |
try: | |
scope[alias] = auto_module(name) | |
except (ModuleNotFoundError, AttributeError) as e: | |
fails.append({"line": line, "name": name, "alias": alias, "exception": e}) | |
return fails | |
_from_import_sub = compile(r'^from\s(.*)\simport\s').sub | |
_import_sub = compile(r'^import\s').sub | |
_as_match = compile(r'^(.*)\sas\s(.*)').match | |
def line_to_names(line: _string_type, encoding="utf-8"): | |
if isinstance(line, bytes): | |
line = line.decode(encoding=encoding) | |
level1 = _import_sub('', _from_import_sub(r'\1.', line.lstrip())) | |
if matches := _as_match(level1): | |
return [matches.group(1)] | |
else: | |
level2 = level1.split(',') | |
singles = [] | |
for i, ref in enumerate(level2): | |
if i: | |
singles.append(ref.rsplit('.', 1)[-1]) | |
else: | |
singles.append(ref.lstrip()) | |
return singles | |
def line_to_aliases(line: _string_type, names: List[str] = None, encoding="utf-8"): | |
if isinstance(line, bytes): | |
line = line.decode(encoding=encoding) | |
if alias := _as_match(line.lstrip()): | |
return [alias.group(2)] | |
else: | |
if names is None: | |
names = line_to_names(line) | |
aliases = [] | |
for name in names: | |
aliases.append(name.rsplit('.', 1)[-1]) | |
return aliases | |
def module_from_file(name: str, full_path: _path_type): | |
""" | |
Import the module in 'full_path' with the given 'name' and return it. | |
:param name: the (arbitrary) module's name e.g. 'my_package.my_module' | |
:param full_path: full path to the python module origin e.g. '/my_dir/my_package/my_module.py' | |
:raise FileNotFoundError: if the file does not exist | |
:raise Exception: any internal module error | |
>>> my_module = module_from_file('my_module', '/my_dir/my_module/__init__.py') | |
""" | |
if (spec := spec_from_file_location(name, full_path, submodule_search_locations=[dirname(full_path)])) is not None: | |
m = module_from_spec(spec) | |
modules[name] = m | |
spec.loader.exec_module(m) # noqa | |
return m | |
else: | |
return | |
def import_to(scope: dict, module_name: str, _as=None, obj=False, from_path: _path_type = None, cache=True): | |
""" | |
Add a module/object to the given dictionary. | |
Useful to avoid multiple assign to an already imported variable. | |
:param scope: a dict e.g. globals(), vars(), __dict__ | |
:param module_name: module's (object) name (mandatory even with 'from_path') | |
:param _as: key in 'scope' dict. Default to 'module_name' last name e.g. 'os.path.join' > 'join' | |
:param obj: if 'module_name' refers to an object | |
:param from_path: the module's path if needed | |
:param cache: if the existent key in scope or reference in system should be kept/used | |
:raise ValueError: obj == True but 'module_name' refers to a module | |
:raise ModuleNotFoundError: | |
:raise FileNotFoundError: | |
:raise AttributeError: obj == True but the object reference do not exist in module | |
:raise Exception: any internal module error | |
Import a module to global scope: | |
>>> import_to(globals(), 'os.path') | |
Import a module to global scope with a specific name: | |
>>> import_to(globals(), 'os.path', _as='o_path') | |
Import a module's object to global scope: | |
>>> import_to(globals(), 'os.path.join', obj=True) | |
Import a module from a path to global scope: | |
>>> import_to(globals(), 'my_package.my_module', from_path='my_dir/my_package/my_module.py') | |
Import a module's object from a path to global scope: | |
>>> import_to(globals(), 'my_package.my_module', obj=True, from_path='my_dir/my_package/my_module.py') | |
Import a module again even if its already imported to local scope or in the system: | |
>>> import_to(vars(), 'os.path', cache=False) | |
""" | |
if _as is None: | |
_as = module_name.rsplit('.', 1)[-1] | |
if cache and module_name in scope: | |
return | |
scope[_as] = module(name=module_name, obj=obj, from_path=from_path, cache=cache) | |
_imported_paths = set() | |
def auto_module(reference: _path_type) -> _module_type: | |
""" | |
Try to return a module or a module's object. | |
:param reference: path to module or the module's (object) name | |
:raise ModuleNotFoundError: if 'reference' was not found either way | |
:raise AttributeError: if 'reference' was found as an object inside a module, but this object do not exists | |
:raise Exception: any internal error in the module found | |
>>> unknown0 = auto_module('some_dir/some_file') | |
>>> unknown1 = auto_module('some_module') | |
>>> unknown2 = auto_module('some_module.some_obj') | |
""" | |
from importlib.util import find_spec | |
if (spec := find_spec(reference)) is None: | |
if len(name_alias := reference.rsplit('.', 1)) == 2: | |
name, alias = name_alias | |
if (spec := find_spec(name)) is not None: | |
if hasattr(m := module_from_spec(spec), alias): | |
return getattr(m, alias) | |
else: | |
raise AttributeError(f"Module '{name}' was found but its object '{alias}' wasn't") | |
else: | |
return module_from_spec(spec) | |
path = abspath(reference) | |
if isfile(path): | |
from os.path import basename | |
module_name = basename(reference).rsplit('.', 1)[0] + '_auto_0' | |
if path in _imported_paths: | |
m_counter = 0 | |
while module_name in modules: | |
m_counter += 1 | |
module_name = module_name[:-1] + str(m_counter) | |
del m_counter | |
if (m := module_from_file(module_name, path)) is not None: | |
_imported_paths.add(path) | |
return m | |
raise ModuleNotFoundError(f"Module referenced as '{reference}' was not found") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment