"""
Dynamically generate the import exec
"""
from os.path import dirname, join, exists
import sys
import multiprocessing
import textwrap
[docs]
def dynamic_init(modname, submodules=None, dump=False, verbose=False):
"""
Main entry point for dynamic mkinit.
Dynamically import listed util libraries and their attributes.
Create reload_subs function.
Using __import__ like this is typically not considered good style However,
it is better than import * and this will generate the good file text that
can be used when the module is 'frozen"
Note:
Dynamic mkinit is for initial development and prototyping, and even
then it is not recommended. For production it is strongly recommended
to use static mkinit instead of dynamic mkinit.
Example:
>>> # The easiest way to use this in your code is to add these lines
>>> # to the module __init__ file
>>> from mkinit import dynamic_init
>>> execstr = dynamic_init('mkinit')
>>> print(execstr)
>>> exec(execstr) # xdoc: +SKIP
"""
if verbose:
print("[MKINIT] Running Dynamic Imports for modname=%r " % modname)
# Get the module that will be imported into
try:
module = sys.modules[modname]
except Exception:
module = __import__(modname)
if submodules is None:
pkgpath = dirname(module.__file__)
submodules = _find_local_submodule_names(pkgpath)
imports = submodules
# Import the modules
_excecute_imports(module, modname, imports, verbose=verbose)
# If developing do explicit import stars
from_imports = _execute_fromimport_star(module, modname, imports, verbose=verbose)
# If requested: print what the __init__ module should look like
dump_requested = (
("--dump-%s-init" % modname) in sys.argv
or ("--print-%s-init" % modname) in sys.argv
) or dump
overwrite_requested = ("--update-%s-init" % modname) in sys.argv
if verbose:
print("[MKINIT] Finished Dynamic Imports for modname=%r " % modname)
initstr = _make_initstr(modname, imports, from_imports, withheader=False)
if dump_requested:
is_main_proc = multiprocessing.current_process().name == "MainProcess"
if is_main_proc:
_initstr = _make_initstr(modname, imports, from_imports)
print(_indent(_initstr))
# Overwrite the __init__.py file with new explicit imports
if overwrite_requested:
is_main_proc = multiprocessing.current_process().name == "MainProcess"
if is_main_proc:
modpath = module.__path__[0]
_autogen_write(modpath, _indent(initstr))
return initstr
[docs]
def _indent(str_, indent=" "):
return indent + str_.replace("\n", "\n" + indent)
[docs]
def _excecute_imports(module, modname, imports, verbose=False):
""" Module Imports """
# level: -1 is a the Python2 import strategy
# level: 0 is a the Python3 absolute import
if verbose:
print("[MKINIT] EXECUTING %d IMPORT TUPLES" % (len(imports),))
level = 0
for name in imports:
if level == -1:
tmp = __import__(name, globals(), locals(), fromlist=[], level=level)
elif level == 0:
# FIXME: should support unicode. Maybe just a python2 thing
tmp = __import__(
modname, globals(), locals(), fromlist=[str(name)], level=level
)
[docs]
def _execute_fromimport_star(
module, modname, imports, check_not_imported=False, verbose=False
):
"""
Effectively import * statements
The dynamic_init must happen before any * imports otherwise it wont catch
anything.
"""
if verbose:
print("[MKINIT] EXECUTE %d FROMIMPORT STAR TUPLES." % (len(imports),))
from_imports = []
# Explicitly ignore these special functions (usually stdlib functions)
# FIXME: find a better way to do this
ignoreset = set(
[
"print",
"print_function",
"absolute_import",
"division",
"zip",
"map",
"range",
"list",
"zip_longest",
"filter",
"filterfalse",
"dirname",
"realpath",
"join",
"exists",
"normpath",
"splitext",
"expanduser",
"relpath",
"isabs",
"commonprefix",
"basename",
"input",
"reduce",
]
)
for name in imports:
# absname = modname + '.' + name
child_module = sys.modules[modname + "." + name]
# Check if the variable already belongs to the module
varset = set(vars(module)) if check_not_imported else set()
fromset = set() # set(fromlist) if fromlist is not None else set()
def valid_attrname(attrname):
"""
Guess if the attrname is valid based on its name
"""
is_forced = attrname in fromset
is_private = attrname.startswith("_")
is_conflit = attrname in varset
is_module = attrname in sys.modules # Isn't fool proof (next step is)
is_ignore = attrname in ignoreset
is_valid = not any((is_ignore, is_private, is_conflit, is_module))
return is_forced or is_valid
if hasattr(child_module, "__all__"):
from_imports.append((name, getattr(child_module, "__all__")))
continue
allattrs = dir(child_module)
fromlist_ = [attrname for attrname in allattrs if valid_attrname(attrname)]
valid_fromlist_ = []
for attrname in fromlist_:
attrval = getattr(child_module, attrname)
try:
# Disallow fromimport modules
forced = attrname in fromset
if not forced and getattr(attrval, "__name__") in sys.modules:
if verbose > 1:
print("[MKINIT] not importing: %r" % attrname)
continue
except AttributeError:
pass
if verbose > 1:
print("[MKINIT] %s is importing: %r" % (modname, attrname))
valid_fromlist_.append(attrname)
setattr(module, attrname, attrval)
if verbose:
print(
"[MKINIT] name=%r, len(valid_fromlist_)=%d"
% (name, len(valid_fromlist_))
)
from_imports.append((name, valid_fromlist_))
return from_imports
[docs]
def _make_initstr(modname, imports, from_imports, withheader=True):
""" Calls the other string makers """
header = _make_module_header() if withheader else ""
import_str = _make_imports_str(imports, modname)
fromimport_str = _make_fromimport_str(from_imports, modname)
initstr = "\n".join(
[
str_
for str_ in [
header,
import_str,
fromimport_str,
]
if len(str_) > 0
]
)
return initstr
[docs]
def _make_imports_str(imports, rootmodname="."):
imports_fmtstr = "from {rootmodname} import %s".format(rootmodname=rootmodname)
return "\n".join([imports_fmtstr % (name,) for name in imports])
[docs]
def _make_fromimport_str(from_imports, rootmodname="."):
if rootmodname == ".":
# dot is already taken care of in fmtstr
rootmodname = ""
def _pack_fromimport(tup):
name, fromlist = tup[0], tup[1]
from_module_str = "from {rootmodname}.{name} import (".format(
rootmodname=rootmodname, name=name
)
newline_prefix = " " * len(from_module_str)
if len(fromlist) > 0:
rawstr = from_module_str + ", ".join(fromlist) + ",)"
else:
rawstr = ""
# not sure why this isn't 76? >= maybe?
packstr = "\n".join(
textwrap.wrap(
rawstr,
break_long_words=False,
width=79,
initial_indent="",
subsequent_indent=newline_prefix,
)
)
return packstr
from_str = "\n".join(map(_pack_fromimport, from_imports))
return from_str
[docs]
def _find_local_submodule_names(pkgpath):
# Automatically find the imports if they are not specified
from mkinit import static_mkinit
import_paths = dict(static_mkinit._find_local_submodules(pkgpath))
imports = list(import_paths.keys())
return imports
[docs]
def _autogen_write(modpath, initstr):
"""
TODO:
- [ ] : replace with code in mkinit/formatting.py
"""
# Get path to init file so we can overwrite it
init_fpath = join(modpath, "__init__.py")
print("attempting to update: %r" % init_fpath)
assert exists(init_fpath)
new_lines = []
editing = False
updated = False
with open(init_fpath, "r") as file_:
lines = file_.readlines()
for line in lines:
if not editing:
new_lines.append(line)
if line.strip().startswith("# <AUTOGEN_INIT>"):
new_lines.append("\n" + initstr + "\n # </AUTOGEN_INIT>\n")
editing = True
updated = True
if line.strip().startswith("# </AUTOGEN_INIT>"):
editing = False
if updated:
print("writing updated file: %r" % init_fpath)
new_text = "".join(new_lines)
with open(init_fpath, "w") as file_:
file_.write(new_text)
else:
print("no write hook for file: %r" % init_fpath)