Skip to content

Instantly share code, notes, and snippets.

@ntaraujo
Last active March 16, 2021 12:44
Show Gist options
  • Save ntaraujo/f9454487dc69fcd81ba31178772d1434 to your computer and use it in GitHub Desktop.
Save ntaraujo/f9454487dc69fcd81ba31178772d1434 to your computer and use it in GitHub Desktop.
import module/object referenced as string/path
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