# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
#
# lazy_import_plus --- https://github.com/mnmelo/lazy_import_plus
# Copyright (C) 2017-2018 Manuel Nuno Melo
#
# This file is part of lazy_import_plus.
#
#  lazy_import_plus is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  lazy_import_plus is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with lazy_import_plus.  If not, see <http://www.gnu.org/licenses/>.
#
# lazy_import_plus was based on code from the importing module from the PEAK
# package (see <http://peak.telecommunity.com/DevCenter/Importing>). The PEAK
# package is released under the following license, reproduced here:
#
#  Copyright (C) 1996-2004 by Phillip J. Eby and Tyler C. Sarna.
#  All rights reserved.  This software may be used under the same terms
#  as Zope or Python.  THERE ARE ABSOLUTELY NO WARRANTIES OF ANY KIND.
#  Code quality varies between modules, from "beta" to "experimental
#  pre-alpha".  :)
#
# Code pertaining to lazy loading from PEAK importing was included in
# lazy_import_plus, modified in a number of ways. These are detailed in the
# CHANGELOG file of lazy_import_plus. Changes mainly involved Python 3
# compatibility, extension to allow customizable behavior, and added
# functionality (lazy importing of callable objects).
# 

"""
Lazy module loading
===================

Functions and classes for lazy module loading that also delay import errors.
Heavily borrowed from the `importing`_ module.

.. _`importing`: http://peak.telecommunity.com/DevCenter/Importing

Files and directories
---------------------

.. autofunction:: module
.. autofunction:: callable

"""

__all__ = ['lazy_module', 'lazy_callable', 'lazy_function', 'lazy_class',
           'LazyModule', 'LazyCallable', 'module_basename', '_MSG',
           '_MSG_CALLABLE']

from types import ModuleType
import sys
try:
    from importlib._bootstrap import _ImportLockContext
except ImportError:
    # Python 2 doesn't have the context manager. Roll it ourselves (copied from
    # Python 3's importlib/_bootstrap.py)
    import imp
    class _ImportLockContext:
        """Context manager for the import lock."""
        def __enter__(self):
            imp.acquire_lock()
        def __exit__(self, exc_type, exc_value, exc_traceback):
            imp.release_lock()


# Adding a __spec__ doesn't really help. I'll leave the code here in case
# future python implementations start relying on it.
#try:
#    from importlib.machinery import ModuleSpec
#except ImportError:
#    ModuleSpec = None

import six
from six import raise_from
from six.moves import reload_module
# It is sometime useful to have access to the version number of a library.
# This is usually done through the __version__ special attribute.
# To make sure the version number is consistent between setup.py and the
# library, we read the version number from the file called VERSION that stays
# in the module directory.
import os
VERSION_FILE = os.path.join(os.path.dirname(__file__), 'VERSION')
with open(VERSION_FILE) as infile:
    __version__ = infile.read().strip()

# Logging
import logging
# adding a TRACE level for stack debugging
_LAZY_TRACE = 1
logging.addLevelName(1, "LAZY_TRACE")
logging.basicConfig(level=logging.WARNING)
# Logs a formatted stack (takes no message or args/kwargs)
def _lazy_trace(self):
    if self.isEnabledFor(_LAZY_TRACE):
        import traceback
        self._log(_LAZY_TRACE, " ### STACK TRACE ###", ())
        for line in traceback.format_stack(sys._getframe(2)):
            for subline in line.split("\n"):
                self._log(_LAZY_TRACE, subline.rstrip(), ())
logging.Logger.lazy_trace = _lazy_trace
logger = logging.getLogger(__name__)

import weakref
from lazy_import_plus.lazy_class import lazy_class

################################
# Module/function registration #
################################

#### Lazy classes ####

overrides = weakref.WeakKeyDictionary()

class LazyModule(ModuleType):
    """Class for lazily-loaded modules that triggers proper loading on access.

    Instantiation should be made from a subclass of :class:`LazyModule`, with
    one subclass per instantiated module. Regular attribute set/access can then
    be recovered by setting the subclass's :meth:`__getattribute__` and
    :meth:`__setattribute__` to those of :class:`types.ModuleType`.
    """
    # peak.util.imports sets __slots__ to (), but it seems pointless because
    # the base ModuleType doesn't itself set __slots__.
    def __getattribute__(self, attr):
        if (overriden := overrides.get(self)) is not None and attr in overriden:
            return overriden[attr]

        logger.debug("Getting attr {} of LazyModule instance of {}"
                     .format(attr, super(LazyModule, self)
                             .__getattribute__("__name__")))
        logger.lazy_trace()
        # IPython tries to be too clever and constantly inspects, asking for
        #  modules' attrs, which causes premature module loading and unesthetic
        #  internal errors if the lazily-loaded module doesn't exist.
        if (run_from_ipython()
            and (attr.startswith(("__", "_ipython"))
                 or attr == "_repr_mimebundle_")
            and module_basename(_caller_name()) in ('inspect', 'IPython')):
                logger.debug("Ignoring request for {}, deemed from IPython's "
                             "inspection.".format(super(LazyModule, self)
                                     .__getattribute__("__name__"), attr))
                raise AttributeError
        if not attr in ('__name__','__class__','__spec__'):
            # __name__ and __class__ yield their values from the LazyModule;
            # __spec__ causes an AttributeError. Maybe in the future it will be
            # necessary to return an actual ModuleSpec object, but it works as
            # it is without that now.

            # If it's an already-loaded submodule, we return it without
            # triggering a full loading
            try:
                return sys.modules[self.__name__+"."+attr]
            except KeyError:
                pass
            # Check if it's one of the lazy callables
            try:
                _callable = type(self)._lazy_import_plus_callables[attr]
                logger.debug("Returning lazy-callable '{}'.".format(attr))
                return _callable
            except (AttributeError, KeyError) as err:
                logger.debug("Proceeding to load module {}, "
                             "from requested value {}"
                             .format(super(LazyModule, self)
                                     .__getattribute__("__name__"), attr))
                _load_module(self)
        logger.debug("Returning value '{}'.".format(super(LazyModule, self)
                                     .__getattribute__(attr)))
        return super(LazyModule, self).__getattribute__(attr)

    def __setattr__(self, attr, value):
        logger.debug("Setting attr {} to value {}, in LazyModule instance "
                     "of {}".format(attr, value, super(LazyModule, self)
                                    .__getattribute__("__name__")))
        _load_module(self)
        return super(LazyModule, self).__setattr__(attr, value)


class LazyCallable(object):
    """Class for lazily-loaded callables that triggers module loading on access

    """
    def __init__(self, *args):
        if len(args) != 2:
            # Maybe the user tried to base a class off this lazy callable?
            try:
                logger.debug("Got wrong number of args when init'ing "
                             "LazyCallable. args is '{}'".format(args))
                base = args[1][0]
                if isinstance(base, LazyCallable) and len(args) == 3:
                    raise NotImplementedError("It seems you are trying to use "
                                              "a lazy callable as a class "
                                              "base. This is not supported.")
            except (IndexError, TypeError):
                raise_from(TypeError("LazyCallable takes exactly 2 arguments: "
                                "a module/lazy module object and the name of "
                                "a callable to be lazily loaded."), None)
        self.module, self.cname = args
        self.modclass = type(self.module)
        self.callable = None
        # Need to save these, since the module-loading gets rid of them
        self.error_msgs = self.modclass._lazy_import_plus_error_msgs
        self.error_strings = self.modclass._lazy_import_plus_error_strings

    def __call__(self, *args, **kwargs):
        # No need to go through all the reloading more than once.
        if self.callable:
            return self.callable(*args, **kwargs)
        try:
            del self.modclass._lazy_import_plus_callables[self.cname]
        except (AttributeError, KeyError):
            pass
        try:
            self.callable = getattr(self.module, self.cname)
        except AttributeError:
            msg = self.error_msgs['msg_callable']
            raise_from(AttributeError(
                msg.format(callable=self.cname, **self.error_strings)), None)
        except ImportError as err:
            # Import failed. We reset the dict and re-raise the ImportError.
            try:
                self.modclass._lazy_import_plus_callables[self.cname] = self
            except AttributeError:
                self.modclass._lazy_import_plus_callables = {self.cname: self}
            raise_from(err, None)
        else:
            return self.callable(*args, **kwargs)


### Functions ###

def lazy_module(modname, error_strings=None, lazy_mod_class=LazyModule,
                  level='leaf', attrs=None):
    """Function allowing lazy importing of a module into the namespace.

    A lazy module object is created, registered in `sys.modules`, and
    returned. This is a hollow module; actual loading, and `ImportErrors` if
    not found, are delayed until an attempt is made to access attributes of the
    lazy module.

    A handy application is to use :func:`lazy_module` early in your own code
    (say, in `__init__.py`) to register all modulenames you want to be lazy.
    Because of registration in `sys.modules` later invocations of
    `import modulename` will also return the lazy object. This means that after
    initial registration the rest of your code can use regular pyhon import
    statements and retain the lazyness of the modules.

    Parameters
    ----------
    modname : str
         The module to import.
    error_strings : dict, optional
         A dictionary of strings to use when module-loading fails. Key 'msg'
         sets the message to use (defaults to :attr:`lazy_import_plus._MSG`). The
         message is formatted using the remaining dictionary keys. The default
         message informs the user of which module is missing (key 'module'),
         what code loaded the module as lazy (key 'caller'), and which package
         should be installed to solve the dependency (key 'install_name').
         None of the keys is mandatory and all are given smart names by default.
    lazy_mod_class: type, optional
         Which class to use when instantiating the lazy module, to allow
         deep customization. The default is :class:`LazyModule` and custom
         alternatives **must** be a subclass thereof.
    level : str, optional
         Which submodule reference to return. Either a reference to the 'leaf'
         module (the default) or to the 'base' module. This is useful if you'll
         be using the module functionality in the same place you're calling
         :func:`lazy_module` from, since then you don't need to run `import`
         again. Setting *level* does not affect which names/modules get
         registered in `sys.modules`.
         For *level* set to 'base' and *modulename* 'aaa.bbb.ccc'::

            aaa = lazy_import_plus.lazy_module("aaa.bbb.ccc", level='base')
            # 'aaa' becomes defined in the current namespace, with
            #  (sub)attributes 'aaa.bbb' and 'aaa.bbb.ccc'.
            # It's the lazy equivalent to:
            import aaa.bbb.ccc

        For *level* set to 'leaf'::

            ccc = lazy_import_plus.lazy_module("aaa.bbb.ccc", level='leaf')
            # Only 'ccc' becomes set in the current namespace.
            # Lazy equivalent to:
            from aaa.bbb import ccc
    attrs : dict, optional
         A dictionary of attributes for the lazy module to use. Accessing
         these attributes won't result in the real module being loaded. Defaults
         to dict(__file__=None), which is needed if later importing torch, since it
         checks for a __file__ attribute on every loaded module upon import.

    Returns
    -------
    module
        The module specified by *modname*, or its base, depending on *level*.
        The module isn't immediately imported. Instead, an instance of
        *lazy_mod_class* is returned. Upon access to any of its attributes, the
        module is finally loaded.

    Examples
    --------
    >>> import lazy_import_plus, sys
    >>> np = lazy_import_plus.lazy_module("numpy")
    >>> np
    Lazily-loaded module numpy
    >>> np is sys.modules['numpy']
    True
    >>> np.pi # This causes the full loading of the module ...
    3.141592653589793
    >>> np # ... and the module is changed in place. 
    <module 'numpy' from '/usr/local/lib/python/site-packages/numpy/__init__.py'>

    >>> import lazy_import_plus, sys
    >>> # The following succeeds even when asking for a module that's not available
    >>> missing = lazy_import_plus.lazy_module("missing_module")
    >>> missing
    Lazily-loaded module missing_module
    >>> missing is sys.modules['missing_module']
    True
    >>> missing.some_attr # This causes the full loading of the module, which now fails.
    ImportError: __main__ attempted to use a functionality that requires module missing_module, but it couldn't be loaded. Please install missing_module and retry.

    See Also
    --------
    :func:`lazy_callable`
    :class:`LazyModule`

    """
    if error_strings is None:
        error_strings = {}
    _set_default_errornames(modname, error_strings)

    if attrs is None:
        attrs = dict(__file__=None)
    mod = _lazy_module(modname, error_strings, lazy_mod_class, attrs=attrs)
    if level == 'base':
        return sys.modules[module_basename(modname)]
    elif level == 'leaf':
        return mod
    else:
        raise ValueError("Parameter 'level' must be one of ('base', 'leaf')")


def _lazy_module(modname, error_strings, lazy_mod_class, attrs):
    with _ImportLockContext():
        fullmodname = modname
        fullsubmodname = None
        # ensure parent module/package is in sys.modules
        # and parent.modname=module, as soon as the parent is imported   
        while modname:
            try:
                mod = sys.modules[modname]
                # We reached a (base) module that's already loaded. Let's stop
                # the cycle. Can't use 'break' because we still want to go 
                # through the fullsubmodname check below.
                modname = ''
            except KeyError:
                err_s = error_strings.copy()
                err_s.setdefault('module', modname)

                class _LazyModule(lazy_mod_class):
                    _lazy_import_plus_error_msgs = {'msg': err_s.pop('msg')}
                    try:
                        _lazy_import_plus_error_msgs['msg_callable'] = \
                                err_s.pop('msg_callable')
                    except KeyError:
                        pass
                    _lazy_import_plus_error_strings = err_s
                    _lazy_import_plus_callables = {}
                    _lazy_import_plus_submodules = {}

                    def __repr__(self):
                        return "Lazily-loaded module {}".format(self.__name__)
                # A bit of cosmetic, to make AttributeErrors read more natural  
                _LazyModule.__name__ = 'module'
                # Actual module instantiation
                mod = sys.modules[modname] = _LazyModule(modname)
                overrides[mod] = attrs
                # No need for __spec__. Maybe in the future.
                #if ModuleSpec:
                #    ModuleType.__setattr__(mod, '__spec__',
                #            ModuleSpec(modname, None))
            if fullsubmodname:
                submod = sys.modules[fullsubmodname]
                ModuleType.__setattr__(mod, submodname, submod)
                _LazyModule._lazy_import_plus_submodules[submodname] = submod
            fullsubmodname = modname
            modname, _, submodname = modname.rpartition('.')
        return sys.modules[fullmodname]


def lazy_callable(modname, *names, **kwargs):
    """Performs lazy importing of one or more callables.

    :func:`lazy_callable` creates functions that are thin wrappers that pass
    any and all arguments straight to the target module's callables. These can
    be functions or classes. The full loading of that module is only actually
    triggered when the returned lazy function itself is called. This lazy
    import of the target module uses the same mechanism as
    :func:`lazy_module`.
    
    If, however, the target module has already been fully imported prior
    to invocation of :func:`lazy_callable`, then the target callables
    themselves are returned and no lazy imports are made.

    :func:`lazy_function` and :func:`lazy_function` are aliases of
    :func:`lazy_callable`.

    Parameters
    ----------
    modname : str
         The base module from where to import the callable(s) in *names*,
         or a full 'module_name.callable_name' string.
    names : str (optional)
         The callable name(s) to import from the module specified by *modname*.
         If left empty, *modname* is assumed to also include the callable name
         to import.
    error_strings : dict, optional
         A dictionary of strings to use when reporting loading errors (either a
         missing module, or a missing callable name in the loaded module).
         *error_string* follows the same usage as described under
         :func:`lazy_module`, with the exceptions that 1) a further key,
         'msg_callable', can be supplied to be used as the error when a module
         is successfully loaded but the target callable can't be found therein
         (defaulting to :attr:`lazy_import_plus._MSG_CALLABLE`); 2) a key 'callable'
         is always added with the callable name being loaded.
    lazy_mod_class : type, optional
         See definition under :func:`lazy_module`.
    lazy_call_class : type, optional
         Analogously to *lazy_mod_class*, allows setting a custom class to
         handle lazy callables, other than the default :class:`LazyCallable`.

    Returns
    -------
    wrapper function or tuple of wrapper functions
        If *names* is passed, returns a tuple of wrapper functions, one for
        each element in *names*.
        If only *modname* is passed it is assumed to be a full
        'module_name.callable_name' string, in which case the wrapper for the
        imported callable is returned directly, and not in a tuple.
        
    Notes
    -----
    Unlike :func:`lazy_module`, which returns a lazy module that eventually
    mutates into the fully-functional version, :func:`lazy_callable` only
    returns thin wrappers that never change. This means that the returned
    wrapper object never truly becomes the one under the module's namespace,
    even after successful loading of the module in *modname*. This is fine for
    most practical use cases, but may break code that relies on the usage of
    the returned objects oter than calling them. One such example is the lazy
    import of a class: it's fine to use the returned wrapper to instantiate an
    object, but it can't be used, for instance, to subclass from.

    Examples
    --------
    >>> import lazy_import_plus, sys
    >>> fn = lazy_import_plus.lazy_callable("numpy.arange")
    >>> sys.modules['numpy']
    Lazily-loaded module numpy
    >>> fn(10)
    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    >>> sys.modules['numpy']
    <module 'numpy' from '/usr/local/lib/python3.5/site-packages/numpy/__init__.py'>

    >>> import lazy_import_plus, sys
    >>> cl = lazy_import_plus.lazy_callable("numpy.ndarray") # a class
    >>> obj = cl([1, 2]) # This works OK (and also triggers the loading of numpy)
    >>> class MySubclass(cl): # This fails because cls is just a wrapper,
    >>>     pass              #  not an actual class.

    See Also
    --------
    :func:`lazy_module`
    :class:`LazyCallable`
    :class:`LazyModule`

    """
    if not names:
        modname, _, name = modname.rpartition(".")
    lazy_mod_class = _setdef(kwargs, 'lazy_mod_class', LazyModule)
    lazy_call_class = _setdef(kwargs, 'lazy_call_class', LazyCallable)
    error_strings = _setdef(kwargs, 'error_strings', {})
    _set_default_errornames(modname, error_strings, call=True)

    if not names:
        # We allow passing a single string as 'modname.callable_name',
        # in which case the wrapper is returned directly and not as a list.
        return _lazy_callable(modname, name, error_strings.copy(),
                                lazy_mod_class, lazy_call_class)
    return tuple(_lazy_callable(modname, cname, error_strings.copy(),
                        lazy_mod_class, lazy_call_class) for cname in names)

lazy_function = lazy_callable

def _lazy_callable(modname, cname, error_strings,
                     lazy_mod_class, lazy_call_class):
    # We could do most of this in the LazyCallable __init__, but here we can
    # pre-check whether to actually be lazy or not.
    module = _lazy_module(modname, error_strings, lazy_mod_class, attrs={})
    modclass = type(module)
    if (issubclass(modclass, LazyModule) and
        hasattr(modclass, '_lazy_import_plus_callables')):
        modclass._lazy_import_plus_callables.setdefault(
            cname, lazy_call_class(module, cname))
    return getattr(module, cname)


#######################
# Real module loading #
#######################

def _load_module(module):
    """Ensures that a module, and its parents, are properly loaded

    """
    modclass = type(module)
    # We only take care of our own LazyModule instances
    if not issubclass(modclass, LazyModule):
        raise TypeError("Passed module is not a LazyModule instance.")
    with _ImportLockContext():
        parent, _, modname = module.__name__.rpartition('.')
        logger.debug("loading module {}".format(modname))
        # We first identify whether this is a loadable LazyModule, then we
        # strip as much of lazy_import_plus behavior as possible (keeping it cached,
        # in case loading fails and we need to reset the lazy state).
        if not hasattr(modclass, '_lazy_import_plus_error_msgs'):
            # Alreay loaded (no _lazy_import_plus_error_msgs attr). Not reloading.
            return
        # First, ensure the parent is loaded (using recursion; *very* unlikely
        # we'll ever hit a stack limit in this case).
        modclass._LOADING = True
        try:
            if parent:
                logger.debug("first loading parent module {}".format(parent))
                setattr(sys.modules[parent], modname, module)
            if not hasattr(modclass, '_LOADING'):
                logger.debug("Module {} already loaded by the parent"
                             .format(modname))
                # We've been loaded by the parent. Let's bail.
                return
            cached_data = _clean_lazymodule(module)
            try:
                # Get Python to do the real import!
                reload_module(module)           
            except:
                # Loading failed. We reset our lazy state.
                logger.debug("Failed to load module {}. Resetting..."
                             .format(modname))
                _reset_lazymodule(module, cached_data)
                raise
            else:
                # Successful load
                logger.debug("Successfully loaded module {}".format(modname))
                delattr(modclass, '_LOADING')
                _reset_lazy_submod_refs(module)

        except (AttributeError, ImportError) as err:
            logger.debug("Failed to load {}.\n{}: {}"
                         .format(modname, err.__class__.__name__, err))
            logger.lazy_trace()
            # Under Python 3 reloading our dummy LazyModule instances causes an
            # AttributeError if the module can't be found. Would be preferrable
            # if we could always rely on an ImportError. As it is we vet the
            # AttributeError as thoroughly as possible.
            if ((six.PY3 and isinstance(err, AttributeError)) and not
                err.args[0] == "'NoneType' object has no attribute 'name'"):
                # Not the AttributeError we were looking for.
                raise
            msg = modclass._lazy_import_plus_error_msgs['msg']
            raise_from(ImportError(
                msg.format(**modclass._lazy_import_plus_error_strings)), None)


##############################
# Helper functions/constants #
##############################

_MSG = ("{caller} attempted to use a functionality that requires module "
        "{module}, but it couldn't be loaded. Please install {install_name} "
        "and retry.")

_MSG_CALLABLE = ("{caller} attempted to use a functionality that requires "
           "{callable}, of module {module}, but it couldn't be found in that "
           "module. Please install a version of {install_name} that has "
           "{module}.{callable} and retry.")

_CLS_ATTRS = ("_lazy_import_plus_error_strings", "_lazy_import_plus_error_msgs",
              "_lazy_import_plus_callables", "_lazy_import_plus_submodules", "__repr__")

_DELETION_DICT = ("_lazy_import_plus_submodules",)

def _setdef(argdict, name, defaultvalue):
    """Like dict.setdefault but sets the default value also if None is present.

    """
    if not name in argdict or argdict[name] is None:
        argdict[name] = defaultvalue
    return argdict[name]


def module_basename(modname):
    return modname.partition('.')[0]


def _set_default_errornames(modname, error_strings, call=False):
    # We don't set the modulename default here because it will change for
    # parents of lazily imported submodules.
    error_strings.setdefault('caller', _caller_name(3, default='Python'))
    error_strings.setdefault('install_name', module_basename(modname))
    error_strings.setdefault('msg', _MSG)
    if call:
        error_strings.setdefault('msg_callable', _MSG_CALLABLE)


def _caller_name(depth=2, default=''):
    """Returns the name of the calling namespace.

    """
    # the presence of sys._getframe might be implementation-dependent.
    # It isn't that serious if we can't get the caller's name.
    try:
        return sys._getframe(depth).f_globals['__name__']
    except AttributeError:
        return default


def _clean_lazymodule(module):
    """Removes all lazy behavior from a module's class, for loading.

    Also removes all module attributes listed under the module's class deletion
    dictionaries. Deletion dictionaries are class attributes with names
    specified in `_DELETION_DICT`.

    Parameters
    ----------
    module: LazyModule 

    Returns
    -------
    dict
        A dictionary of deleted class attributes, that can be used to reset the
        lazy state using :func:`_reset_lazymodule`.
    """
    modclass = type(module)
    _clean_lazy_submod_refs(module)

    modclass.__getattribute__ = ModuleType.__getattribute__
    modclass.__setattr__ = ModuleType.__setattr__
    cls_attrs = {}
    for cls_attr in _CLS_ATTRS:
        try:
            cls_attrs[cls_attr] = getattr(modclass, cls_attr)
            delattr(modclass, cls_attr)
        except AttributeError:
            pass
    return cls_attrs


def _clean_lazy_submod_refs(module):
    modclass = type(module)
    for deldict in _DELETION_DICT:
        try:
            delnames = getattr(modclass, deldict)
        except AttributeError:
            continue
        for delname in delnames:
            try:
                super(LazyModule, module).__delattr__(delname)
            except AttributeError:
                # Maybe raise a warning?
                pass


def _reset_lazymodule(module, cls_attrs):
    """Resets a module's lazy state from cached data.

    """
    modclass = type(module)
    del modclass.__getattribute__
    del modclass.__setattr__
    try:
        del modclass._LOADING
    except AttributeError:
        pass
    for cls_attr in _CLS_ATTRS:
        try:
            setattr(modclass, cls_attr, cls_attrs[cls_attr])
        except KeyError:
            pass
    _reset_lazy_submod_refs(module)


def _reset_lazy_submod_refs(module):
    modclass = type(module)
    for deldict in _DELETION_DICT:
        try:
            resetnames = getattr(modclass, deldict)
        except AttributeError:
            continue
        for name, submod in resetnames.items(): 
            super(LazyModule, module).__setattr__(name, submod)


def run_from_ipython():
    # Taken from https://stackoverflow.com/questions/5376837
    try:
        __IPYTHON__
        return True
    except NameError:
        return False

