# -*- coding: utf-8 -*-
import importlib
import aiida.common
from aiida.common.exceptions import MissingPluginError
__copyright__ = u"Copyright (c), 2015, ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE (Theory and Simulation of Materials (THEOS) and National Centre for Computational Design and Discovery of Novel Materials (NCCR MARVEL)), Switzerland and ROBERT BOSCH LLC, USA. All rights reserved."
__license__ = "MIT license, see LICENSE.txt file"
__version__ = "0.4.1"
__contributors__ = "Andrea Cepellotti, Andrius Merkys, Giovanni Pizzi"
logger = aiida.common.aiidalogger.getChild('pluginloader')
[docs]def get_class_typestring(type_string):
"""
Given the type string, return three strings: the first one is
one of the first-level classes that the Node can be:
"node", "calculation", "code", "data".
The second string is the one that can be passed to the DataFactory or
CalculationFactory (or an empty string for nodes and codes);
the third one is the name of the python class that would be loaded.
"""
from aiida.common.exceptions import DbContentError
if type_string == "":
return ("node", "")
else:
pieces = type_string.split('.')
if pieces[-1]:
raise DbContentError("The type string does not end with a dot")
if len(pieces) < 3:
raise DbContentError("Not enough parts in the type string")
return pieces[0], ".".join(pieces[1:-2]), pieces[-2]
def _existing_plugins_with_module(base_class, plugins_module_path,
pkgname, basename, max_depth, suffix=None):
"""
Recursive function to return the existing plugins within a given module.
:param base_class: Identify all subclasses of the base_class
:param plugins_module_path: The path to the folder with the plugins
:param pkgname: The name of the package in which you want to search
:param basename: The basename of the plugin (sub)class. See also documentation
of ``find_module``.
:param max_depth: Maximum depth (of nested modules) to be used when
looking for plugins
:param suffix: The suffix that is appended to the basename when looking
for the (sub)class name. If not provided (or None), use the base
class name.
:return: a list of valid strings that can be used using a Factory or with
load_plugin.
"""
import pkgutil
import os
if max_depth == 0:
return []
else:
retlist = _find_module(base_class, pkgname, basename, suffix)
for _, name, ismod in pkgutil.walk_packages([plugins_module_path]):
if ismod:
retlist += _existing_plugins_with_module(
base_class, os.path.join(plugins_module_path,name),
"{}.{}".format(pkgname, name),
"{}.{}".format(basename, name) if basename else name,
max_depth-1, suffix=suffix)
# This has to be done anyway, for classes in the __init__ file.
this_pkgname = "{}.{}".format(pkgname, name)
this_basename = "{}.{}".format(basename, name) if basename else name
retlist += _find_module(base_class, this_pkgname, this_basename, suffix)
return list(set(retlist))
def _find_module(base_class, pkgname, this_basename, suffix=None):
"""
Given a base class object, looks for its subclasses inside the package
with name pkgname (must be importable), and prepends to the class name
the string 'this_basename'.
If the name of the class complies with the syntax
AaaBbb
where Aaa is the capitalized name of the containing module (aaa), and
Bbb is base_class.__name__, then only 'aaa' is returned instead of
'aaa.AaaBbb', to have a shorter name that is anyway accepted by the *Factory
functions. If suffix is provided, this is used for comparison (the 'Bbb'
string) rather than the base class name)
:param base_class: Identify all subclasses of the base_class
:param pkgname: The name of the package in which you want to search
:param basename: The basename of the plugin (sub)class. See also documentation
of ``find_module``.
:param suffix: The suffix that is appended to the basename when looking
for the (sub)class name. If not provided (or None), use the base
class name.
:return: a list of valid strings, acceptable by the *Factory functions.
Does not return the class itself.
"""
import inspect
retlist = []
#print ' '*(5-max_depth), '>', pkgname
#print ' '*(5-max_depth), ' ', this_basename
pkg = importlib.import_module(pkgname)
for k, v in pkg.__dict__.iteritems():
if (inspect.isclass(v) and # A class
v != base_class and # Not the class itself
issubclass(v, base_class) and # a proper subclass
pkgname == v.__module__): # We are importing it from its
# module: avoid to import it
# from another module, if it
# was simply imported there
# Try to return the shorter name if the subclass name
# has the correct pattern, as expected by the Factory
# functions
if suffix is None:
actual_suffix = base_class.__name__
else:
actual_suffix = suffix
if k == "{}{}".format(
pkgname.rpartition('.')[2].capitalize(),
actual_suffix):
retlist.append(this_basename)
else:
retlist.append(
("{}.{}".format(this_basename, k) if this_basename
else k))
#print ' '*(5-max_depth), ' ->', "{}.{}".format(this_basename, k)
return retlist
[docs]def existing_plugins(base_class, plugins_module_name, max_depth=5, suffix=None):
"""
Return a list of strings of valid plugins.
:param base_class: Identify all subclasses of the base_class
:param plugins_module_name: a string with the full module name separated
with dots that points to the folder with plugins.
It must be importable by python.
:param max_depth: Maximum depth (of nested modules) to be used when
looking for plugins
:param suffix: The suffix that is appended to the basename when looking
for the (sub)class name. If not provided (or None), use the base
class name.
:return: a list of valid strings that can be used using a Factory or with
load_plugin.
"""
try:
pluginmod = importlib.import_module(plugins_module_name)
except ImportError:
raise MissingPluginError("Unable to load the plugin module {}".format(
plugins_module_name))
return _existing_plugins_with_module(base_class,
pluginmod.__path__[0],
plugins_module_name,
"",
max_depth, suffix)
[docs]def load_plugin(base_class, plugins_module, plugin_type):
"""
Load a specific plugin for the given base class.
This is general and works for any plugin used in AiiDA.
NOTE: actually, now plugins_module and plugin_type are joined with a dot,
and the plugin is retrieved splitting using the last dot of the resulting
string.
TODO: understand if it is probably better to join the two parameters above
to a single one.
Args:
base_class
the abstract base class of the plugin.
plugins_module
a string with the full module name separated with dots
that points to the folder with plugins. It must be importable by python.
plugin_type
the name of the plugin.
Return:
the class of the required plugin.
Raise:
MissingPluginError if the plugin cannot be loaded
Example:
plugin_class = load_plugin(
aiida.transport.Transport,'aiida.transport.plugins','ssh.SshTransport')
and plugin_class will be the class 'aiida.transport.plugins.ssh.SshTransport'
"""
module_name = ".".join([plugins_module,plugin_type])
real_plugin_module, plugin_name = module_name.rsplit('.',1)
try:
pluginmod = importlib.import_module(real_plugin_module)
except ImportError:
raise MissingPluginError("Unable to load the plugin module {}".format(
real_plugin_module))
try:
pluginclass = pluginmod.__dict__[plugin_name]
except KeyError:
raise MissingPluginError("Unable to load the class {} within {}".format(
plugin_name, real_plugin_module))
try:
if issubclass(pluginclass, base_class):
return pluginclass
else:
# Quick way of going into the except case
err_msg = "{} is not a subclass of {}".format(
module_name, base_class.__name__)
raise MissingPluginError(err_msg)
except TypeError:
# This happens when we pass a non-class to issubclass;
err_msg = "{} is not a class".format(
module_name)
raise MissingPluginError(err_msg)
[docs]def BaseFactory(module, base_class, base_modname, suffix=None):
"""
Return a given subclass of Calculation, loading the correct plugin.
:example: If `module='quantumespresso.pw'`, `base_class=JobCalculation`,
`base_modname = 'aiida.orm.calculation.job'`, and `suffix='Calculation'`,
the code will first look for a pw subclass of JobCalculation
inside the quantumespresso module. Lacking such a class, it will try to look
for a 'PwCalculation' inside the quantumespresso.pw module.
In the latter case, the plugin class must have a specific name and be
located in a specific file:
if for instance plugin_name == 'ssh' and base_class.__name__ == 'Transport',
then there must be a class named 'SshTransport' which is a subclass of base_class
in a file 'ssh.py' in the plugins_module folder.
To create the class name to look for, the code will attach the string
passed in the base_modname (after the last dot) and the suffix parameter,
if passed, with the proper CamelCase capitalization. If suffix is not
passed, the default suffix that is used is the base_class class name.
:param module: a string with the module of the plugin to load, e.g.
'quantumespresso.pw'.
:param base_class: a base class from which the returned class should inherit.
e.g.: JobCalculation
:param base_modname: a basic module name, under which the module should be
found. E.g., 'aiida.orm.calculation.job'.
:param suffix: If specified, the suffix that the class name will have.
By default, use the name of the base_class.
"""
try:
return load_plugin(base_class, base_modname, module)
except MissingPluginError as e1:
# Automatically add subclass name and try again
if suffix is None:
actual_suffix = base_class.__name__
else:
actual_suffix = suffix
mname = module.rpartition('.')[2].capitalize() + actual_suffix
new_module = module+ '.' +mname
try:
return load_plugin(base_class, base_modname, new_module)
except MissingPluginError as e2:
err_msg = ("Neither {} or {} could be loaded from {}. "
"Error messages were: '{}', '{}'").format(
module, new_module, base_modname, e1, e2)
raise MissingPluginError(err_msg)