Source code for mkinit.dynamic_mkinit

"""
Dynamically generate the import exec
"""

import multiprocessing
import os
import sys
import textwrap
from os.path import dirname, exists, join


[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: fpath = module.__file__ assert fpath is not None pkgpath = dirname(os.fspath(fpath)) 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_module_header(): return '\n'.join( [ '# flake8:' + ' noqa', # the plus prevents it from triggering on this file 'from __future__ import absolute_import, division, print_function, unicode_literals', ] )
[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)) # type: ignore 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)