# Copyright 2010 (C) Daniel Richman, Adam Greig
#
# This file is part of habitat.
#
# habitat 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.
#
# habitat 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 habitat. If not, see <http://www.gnu.org/licenses/>.
"""
A generic dynamic python module loader.
The main function to call is load(). In addition, several functions
to quickly test the loaded object for certain conditions are provided:
* :func:`isclass`
* :func:`isfunction`
* :func:`isgeneratorfunction`
* :func:`isstandardfunction` (``isfunction and not isgeneratorfunction``)
* :func:`iscallable`
* :func:`issubclass`
* :func:`hasnumargs`
* :func:`hasmethod`
* :func:`hasattr`
Further to that, functions :func:`expectisclass`, :func:`expectisfunction`,
etc., are provided which are identical to the above except they raise either a
ValueError or a TypeError where the original function would have returned
``False``.
Example use::
def loadsomething(loadable):
loadable = dynamicloader.load(loadable)
expectisstandardfunction(loadable)
expecthasattr(loadable, 2)
If you use :func:`expectiscallable` note that you may get either a function
or a class, an object of which is callable (i.e., the class has
``__call__(self, ...))``. In that case you may need to create an object::
if isclass(loadable):
loadable = loadable()
Of course if you've used :func:`expectisclass` then you will be creating an
object anyway. Note that classes are technically "callable" in that calling
them creates objects. :func:`expectiscallable` ignores this.
A lot of the provided tests are imported straight from inspect and are
therefore not documented here. The ones implemented as a part of this
module are.
"""
import sys
import functools
import inspect
import __builtin__
import logging
logger = logging.getLogger("habitat.utils.dynamicloader")
[docs]def load(loadable, force_reload=False):
"""
Attempts to dynamically load *loadable*
*loadable*: a class, a function, a module, or a string that is a
dotted-path to one a class function or module
Some examples::
load(MyClass) # returns MyClass
load(MyFunction) # returns MyFunction
load("mypackage") # returns the mypackage module
load("packagea.packageb") # returns the packageb module
load("packagea.packageb.aclass") # returns aclass
"""
old_modules = sys.modules.keys()
if isinstance(loadable, basestring):
if len(loadable) <= 0:
raise ValueError("loadable(str) must have non zero length")
components = loadable.split(".")
if "" in components or len(components) == 0:
raise ValueError("loadable(str) contains empty components")
name_loaded = loadable
try:
# This will work if it is a module
__import__(loadable)
loadable = sys.modules[loadable]
except ImportError:
# This will work if it is a class or a function
module_name = ".".join(components[:-1])
target_name = components[-1]
__import__(module_name)
try:
loadable = getattr(sys.modules[module_name], target_name)
except KeyError:
raise ImportError("Couldn't import " + loadable)
# If neither worked; an error will have been raised.
name_real = fullname(loadable)
if name_real != name_loaded:
logger.debug("loaded {0} => {1}".format(name_loaded, name_real))
else:
logger.debug("loaded {0}".format(name_real))
# If force_reload is set, but it's the first time we've loaded this
# loadable anyway, there's no point calling reload().
# There could be a race condition between already_loaded and __import__,
# however the worst that could happen is for already_loaded to be False
# when infact by the time __import__ was reached, it had been loaded by
# another thread. In this case the side effect is that reload may be
# called on it. No bad effects, just a slight performance hit from double
# loading. No big deal.
if inspect.isclass(loadable) or inspect.isfunction(loadable):
already_loaded = loadable.__module__ in old_modules
if force_reload and already_loaded:
logger.debug("reloading {0}".format(fullname(loadable)))
# Reload the module and then find the new version of loadable
module = sys.modules[loadable.__module__]
reload(module)
loadable = getattr(module, loadable.__name__)
elif inspect.ismodule(loadable):
already_loaded = loadable.__name__ in old_modules
if force_reload and already_loaded:
logger.debug("reloading {0}".format(fullname(loadable)))
# Module objects are updated in place.
reload(loadable)
else:
raise TypeError("load() takes a string, class, function or module")
return loadable
[docs]def fullname(loadable):
"""
Determines the full name in ``module.module.class`` form
*loadable*: a class, module or function.
If fullname is given a string it will :py:func:`load` it in order to
resolve it to its true full name.
"""
# You can import things into classes from all over the place. Therefore
# you could have two different strings that you can pass to load but
# load the same thing. If fullname() is given a string, rather than
# simply return it, it has to load() it and then figure out what its
# real full name is. See tests
if isinstance(loadable, basestring):
loadable = load(loadable)
if inspect.isclass(loadable) or inspect.isfunction(loadable):
return loadable.__module__ + "." + loadable.__name__
elif inspect.ismodule(loadable):
return loadable.__name__
else:
raise TypeError("loadable isn't class, function, or module")
# Quite a few of the functions we need are provided by Python.
# To keep sphinx happy, we need to explicitly define them here
[docs]def isclass(thing):
"""is *thing* a class?"""
return inspect.isclass(thing)
[docs]def isfunction(thing):
"""is *thing* a function? (either normal or generator)"""
return inspect.isfunction(thing)
[docs]def isgeneratorfunction(thing):
"""is *thing* a generator function?"""
return inspect.isgeneratorfunction(thing)
[docs]def issubclass(thing, the_other_thing):
"""is *thing* a subclass of *the other thing*?"""
return __builtin__.issubclass(thing, the_other_thing)
[docs]def hasattr(thing, attr):
"""does *thing* have an attribute named *attr*?"""
return __builtin__.hasattr(thing, attr)
# Some are very simple
[docs]def isstandardfunction(thing):
"""is *thing* a normal function (i.e., not a generator)"""
return isfunction(thing) and not isgeneratorfunction(thing)
# The following we have to implement ourselves
[docs]def hasnumargs(thing, num):
"""
does *thing* have *num* arguments?
If *thing* is a function, the positional arguments are simply counted up.
If *thing* is a method, the positional arguments are counted up and one
is subtracted in order to account for ``method(self, ...)``
If *thing* is a class, the positional arguments of ``cls.__call__`` are
counted up and one is subtracted (self), giving the number of arguments
a callable object created from that class would have.
"""
# Inspect argument list based on type. Class methods will
# have a self argument, so account for that.
if inspect.isclass(thing):
args = len(inspect.getargspec(thing.__call__).args) - 1
elif inspect.isfunction(thing):
args = len(inspect.getargspec(thing).args)
elif inspect.ismethod(thing):
args = len(inspect.getargspec(thing).args) - 1
else:
return False
return args == num
[docs]def hasmethod(loadable, name):
"""is *loadable.name* callable?"""
try:
expecthasattr(loadable, name)
expectiscallable(getattr(loadable, name))
return True
except:
return False
# Builtin callable() is not good enough since it returns true for any
# class oboject
[docs]def iscallable(loadable):
"""
is *loadable* a method, function or callable class?
For *loadable* to be a callable class, an object created from it must
be callable (i.e., it has a ``__call__`` method)
"""
if inspect.isclass(loadable):
return hasmethod(loadable, "__call__")
else:
return inspect.isroutine(loadable)
def _expectify(error):
"""
Generate an expect function decorator, which will wrap a function and \
raise error rather than return false.
"""
def decorator(function):
def new_function(*args, **kwargs):
if not function(*args, **kwargs):
raise error
functools.update_wrapper(new_function, function)
return new_function
return decorator
expectisclass = _expectify(
TypeError("Not a class"))(isclass)
expectisfunction = _expectify(
TypeError("Not a function"))(isfunction)
expectisgeneratorfunction = _expectify(
TypeError("Not a generator function"))(isgeneratorfunction)
expectisstandardfunction = _expectify(
TypeError("Not a standard function"))(isstandardfunction)
expectiscallable = _expectify(
TypeError("Not callable"))(iscallable)
expectissubclass = _expectify(
TypeError("Not a correct subclass"))(issubclass)
expecthasnumargs = _expectify(
TypeError("Incorrect number of args"))(hasnumargs)
expecthasmethod = _expectify(
TypeError("Does not have a required method"))(hasmethod)
expecthasattr = _expectify(
TypeError("Does not have a required attribute"))(hasattr)