Source code for mmf.utils.init

"""Tools for writing automatic `__init__.py` files.

This allows modules to define a minimal `__init__.py` file with some
flexibility for delaying imports (for performance) and automatically
deducing the local modules.  The function :func:`get_imports` does all
the work and assumes that the module has the following structure::

   mod/__init__.py
       _mod.py
       mod1.py
       ...
       mod2/__init__.py
           ...
       ...
       _mod3/...
       tests/...

These are treated as follows:

   `__init__.py` :
      Should be based on the following skeleton::

         from mmf.utils.init import get_imports as __get_imports
         __delay__ = set([])      # Add modules to delay here.
         __all__, __doc__, __imports = __get_imports(globals())
         exec(__imports)        # Remove this to delay everything.
         del __get_imports, __imports, __delay__
      
      If you want to suppress loading of modules (typically for
      performance) until they are explicitly requested, then add them
      to the `__delay__` set.  If you want to suppress the loading of
      all sub modules, then add the string `'__all__'` to `__delay__`:
      this will still import everything in `_mod.py`.  If you want to
      suppress this also, then don't execute `__imports` and directly
      import the modules you want.

   `_mod.py` :
      This defines all the members directly defined in the module.  The
      following special attributes are used to control the behaviour
      of `__imports` returned by :func:`get_imports`:

      `__all__` :
         If provided, then this is used to define the exported names.
         Only these names will be imported into the module `mod` when
         `__imports` is executed.
      `__dict__` :
         Used if `__all__` is not provided.  This is consistent with the
         default behaviour of `from mod_ import *`.
      `__doc__` :
         The docstring for the module is also obtained from the docstring
         of this file.  Note that some automatic documentation is added
         to the bottom of this for the members that are explicitly
         included.
      `__delay__` :
         If this list is defined, these modules are not explicitly
         imported.  May also contain the special symbol `'__all__'`
         which suppress loading all submodules.

      The file `_mod` is not intended to be exported on it's own,
      which is why we give a "private" name.  If you need access to
      the private module, you can get it from
      `sys.modules['mod._mod']` or set the module flag
      :attr:`_INCLUDE_PRIVATE_MODS` to `True`.  (This behaviour is
      implemented by first importing `mod._mod` and then selectively
      deleting or keeping the attribute `_mod`.

      The module `_mod` will be reloaded if the top-level module is
      reloaded.  Thus calling ``reload(mod)`` will trigger a call to
      ``reload(mod._mod)`` even though this cannot be effected by the
      user (since `_mod` is not in the `mod` namespace).

   `mod1`, `mod2`, ... :
      These are standard modules and will imported in the usual way as
      `import mod1` etc. when `__imports` is executed unless included in
      `__delay__`.

   `_mod3` :
      Names stating with an underscore are private and will be ignored
      unless explicitly included in `__all__`.  Also ignored are names
      with '~' and '#' which represent temporary files.

   `tests` :
      These will be ignored.

Attributes
----------
_INCLUDE_PRIVATE_MOD : bool
   Specifies whether or not the private module '_mod' is
   included in the module or not.  if `True`, then it is
   accessible as `mod._mod` and the `__imports` will include::

     import _mod
     from _mod import *

   If `False`, then only the second line is used and `_mod` is not
   part of the module (however, it will be accessible from
   `sys.modules['mod._mod']` with a fully qualified module name.
"""
from __future__ import division

__all__ = ['get_imports']

import sys
import os.path
import inspect

_INCLUDE_PRIVATE_MODS = False

[docs]def get_imports(globals): """Return `(__all__, __doc__, __imports)` describing the dynamically generated list of imports. The `__init__.py` should look like:: from mmf.utils.init import get_imports as __get_imports __delay__ = set([]) __all__, __doc__, __imports = __get_imports(globals()) exec(__imports) # Remove this to delay everything. del __get_imports, __imports, __delay__ Parameters ---------- globals : dict Global names. Used so that :func:`__import__` will behave as in the original module. Also, `__name__` and `__path__` are used. Returns ------- __all__ : [str] List of imports to use when `from mod import *` is called. __doc__ : str Docstring for module. __imports : str This string should be executed in the context of the module to effect the required imports. Notes ----- This assumes the following structure:: mod/__init__.py _mod.py mod1.py ... mod2/__init__.py ... ... _mod3/... tests/... These are treated as follows `_mod.py` : This defines all the members directly defined in the module. The following special methods are used: `__all__` : If provided, then it is used. `__dict__` : Used if `__all__` is not provided. This is consistent with the behaviour of `from mod_ import *` which is used. `__doc__` : The docstring for the module is also obtained from the docstring of this file. Note that some automatic documentation is added to the bottom of this for the members included. `__delay__` : If this list is defined, these modules are not explicitly imported. It is not intended to be exported on it's own, which is why we give a "private" name. If you need access to the private module, you need to get it from `sys.modules` (or set the module flag :attr:`_INCLUDE_PRIVATE_MODS` to `True`). This module will be reloaded if the top-level module is reloaded. Thus calling ``reload(mod)`` will trigger a call to ``reload(mod._mod)`` even though this cannot be effected by the user (since `_mod` is not in the `mod` namespace). `mod1`, `mod2`, ... : These will be imported unless included in `__delay__`. `_mod3` : Names stating with an underscore are private and will be ignored. Also ignored are names with '~' and '#' which represent temporary files. `tests` : These will be ignored. """ name = globals['__name__'] path = globals['__path__'] __doc__ = globals['__doc__'] __delay__ = set(globals.get('__delay__', set([]))) modname = name.rsplit('.')[-1] modfilename = '_' + modname modfile = modfilename + ".py" imports = [] # Process modfile import_code = """ import %(modfilename)s try: if __is_reload: # This is not defined on first import reload(%(modfilename)s) except NameError: __is_reload = True from %(modfilename)s import * if %(del_local_mod)s: del %(modfilename)s""" if os.path.exists(os.path.join(path[0], modfile)): _m = __import__(modfilename, globals=globals) __delay__.update(getattr(_m, '__delay__', [])) mod_doc = getattr(_m, '__doc__', "") if __doc__ is None: __doc__ = mod_doc else: __doc__ = "\n\n".join([__doc__, mod_doc]) # Make a copy here - we don't want to mutate the original! __all__ = list(getattr(_m, '__all__', getattr(_m, '__dict__', {}).keys())) del_local_mod = True if sys.argv[0].endswith('nosetests') or _INCLUDE_PRIVATE_MODS: # Special case: do not delete the mod if using nosetests! See # issue 11. del_local_mod = False imports.append(import_code % dict( modfilename=modfilename, del_local_mod=str(del_local_mod))) if hasattr(sys.modules[name], modfilename): delattr(sys.modules[name], modfilename) if del_local_mod: # Now remove _modfilename from the end of __module__ for classes so # that the automatic documentation etc. points to the correct place. _objs = [getattr(_m, _class_name, None) for _class_name in __all__] #_classes = [_c for _c in _objs if inspect.isclass(_c)] _tail = "." + modfilename for _o in _objs: if getattr(_o, '__module__', "").endswith(_tail): _o.__module__ = _o.__module__[:-len(_tail)] else: if __doc__ is None: __doc__ = "" __all__ = [] # Process modules modules = [_f for _f in os.listdir(path[0]) if _f != "tests" and _f != "setup.py" and not _f.startswith('_') and not '#' in _f and not '~' in _f and not _f == modfile and (_f.endswith(".py") # Modules or os.path.exists( # Directories os.path.join(path[0], _f, "__init__.py"))) ] modules.sort() for _f in modules: if _f.endswith(".py"): _m = _f[:-3] else: _m = _f if _m in __all__: # Fail if there are any name clashes. raise ImportError("Symbol %s overridden by %s" % ( _m, _f)) else: __all__.append(_m) if _m not in __delay__ and '__all__' not in __delay__: imports.append("import %s" % _m) return __all__, __doc__, "\n".join(imports)