Source code for pyitect.pyitect

from __future__ import (print_function)

import sys
import os
import traceback
import re

PY_VER = sys.version_info[:2]
PY2 = PY_VER[0] == 2
have_importlib = PY_VER >= (3, 4)

if have_importlib:
    import importlib.util
else:
    import imp

import json

import collections
import hashlib

from semantic_version import Version, Spec

# fix types for Python2+ supprot
try:
    basestring
except NameError:
    basestring = str

_have_yaml = False
try:
    import yaml
    _have_yaml = True
except ImportError:
    pass

_system = None


[docs]def get_system(): """Fetch the global system instance Raises: PyitectError: If the system isn't built yet """ global _system if _system: return _system else: raise PyitectError("Global system instance not built yet")
[docs]def build_system(config, enable_yaml=False): """Build a global system instance Args: config (dict): A mapping of component names to version requirements enable_yaml (bool): Should the system support yaml config files? Raises: PyitectError: if the system is already built """ global _system if _system: raise PyitectError("Global system instance already exists") _system = System(config, enable_yaml) return _system
[docs]def destroy_system(): """destroy the global system instance does nothing if the system isn't built """ global _system if _system: _system = None
[docs]class Plugin(object): """An object that can hold the metadata for a plugin like its name, author, verison, and the file to be loaded ect. also stores the path to the plugin folder and provideds functionality to load the plugin module and run its `on_enable` function Attributes: name (str): plugin name author (str): plugin author version (Version): plugin vesion file (str): relative path to the file to import to load the plugin consumes (dict): a listing of the components consumed provides (dict): a listing of the components provided on_enable (None, str): either `None` or a str doted name of a function in the module path (str): an absolute path to the plugin folder module (None, object): either `None` or the modlue object if the plugin has been loaded already """ def __init__(self, config, path): """Init the plugin container object and pull information from it's passed config, storing the path where it can be found Args: config (dict): a mapping object that holds data from a config file path (str): the absolute path to the plugin's folder Raises: ValueError: when any of the config keys are wrong """ if 'name' in config: self.name = config['name'].strip() else: raise ValueError( "Plugin as '%s' does not have a name string" % (path,)) if 'author' in config and isinstance(config['author'], basestring): self.author = config['author'].strip() else: raise ValueError( "Plugin as '%s' does not have a author string" % (path,)) if 'version' in config: # store both the original version string and a parsed version that # can be compaired accurately self.version = gen_version(config['version'].strip()) else: raise ValueError( "Plugin at '%s' does not have a version" % (path,)) if 'file' in config: self.file = config['file'].strip() else: raise ValueError( "Plugin as '%s' does not have a plugin file spesified" % (path,)) if (('consumes' in config) and isinstance(config['consumes'], collections.Mapping)): self.consumes = config['consumes'] else: raise ValueError( "Plugin at '%s' has no map of consumed " "components to plugin versions" % (path,)) if (('provides' in config) and isinstance(config['provides'], collections.Mapping)): self.provides = config['provides'] else: raise ValueError( "Plugin at '%s' hs no map of provided components" " to version postfixes" % (path,)) if 'on_enable' in config: if isinstance(config['on_enable'], basestring): self.on_enable = config['on_enable'] else: raise ValueError( "Plugin at '%s' has a 'on_enable' that is not a string" % (path,)) else: self.on_enable = None self.path = path self.module = None
[docs] def key(self): """return a key that can be used to identify the plugin Returns: tuple: (name, author, version, path) """ return (self.name, self.author, self.version, self.path)
def _load(self): global PY2 # import can handle cases where the file isn't a python source file, # for example a compiled pyhton module in the form of a .pyd or .so # only works with pyhton 3.4+ filepath = os.path.join(self.path, self.file) module_name = get_unique_name(self.author, self.get_version_string()) if have_importlib: try: sys.path.insert(0, self.path) spec = importlib.util.spec_from_file_location( module_name, filepath) plugin = spec.loader.load_module() sys.path.remove(self.path) except Exception as err: raise PyitectLoadError( "Plugin '%s' at '%s' failed to load" % (self.name, self.path), cause=err) else: name = os.path.splitext(os.path.basename(self.file))[0] search_path = self.path if name == "__init__": name = os.path.basename(self.path) search_path = os.path.dirname(self.path) try: sys.path.insert(0, search_path) f, pathn, desc = imp.find_module(name, [search_path]) try: plugin = imp.load_module(module_name, f, pathn, desc) except Exception as err: raise PyitectLoadError( "Plugin '%s' at '%s' failed to load" % (self.name, self.path), cause=err) finally: if f: f.close() sys.path.remove(search_path) except Exception as err: raise PyitectLoadError( "Plugin '%s' at '%s' failed to load" % (self.name, self.path), cause=err) return plugin
[docs] def load(self): """loads the plugin file and returns the resulting module Raises: PyitectLoadError: If there was a problem loading a plugin module PyitectNotProvidedError: If component is not provided """ if self.module is None: plugin = self._load() self.module = plugin return self.module
[docs] def get_version_string(self): """returns a version string""" return self.name + ":" + str(self.version)
[docs] def run_on_enable(self): """runs the function in the 'on_enable' if set Raises: TypeError: if the on_enable property is set wrong PyitectOnEnableError: if there is an exception acessing or calling the on_enable function PyitectLoadError: If the module object is not loaded yet """ if self.on_enable: if not isinstance(self.on_enable, basestring): raise TypeError( "Plugin '%s' at '%s': invalid object path " "in its on_enable" % (self.name, self.path)) parts = self.on_enable.split(".") if self.module is None: raise PyitectLoadError( "Plugin '%s' at '%s': has no module object and is not " "loaded yet. can not attempt to find on_enable function" % (self.name, self.path)) obj = self.module try: for part in parts: obj = getattr(obj, part) except Exception as err: raise PyitectOnEnableError( "Plugin '%s' at '%s': cann't access 'on_enable' path '%s'" % (self.name, self.path, self.on_enable,), cause=err) if not callable(obj): raise PyitectOnEnableError( "Plugin '%s' at '%s': can not call 'on_enable', " "Path '%s', not callable" % (self.name, self.path, self.on_enable,), cause=err) try: obj(self) except Exception as err: raise PyitectOnEnableError( "Plugin '%s' at '%s': Exception during 'on_enable' call" % (self.name, self.path), cause=err) else: raise TypeError( "Plugin '%s' at '%s': has no on_enable" % (self.name, self.path))
[docs] def has_on_enable(self): """returns `True` if it has an `on_enable` attribute that's not None""" return (self.on_enable is not None) and (not self.on_enable == "")
def __str__(self): return self.get_version_string() def __repr__(self): return "Plugin(%s:%s@%s)" % (self.name, self.version, self.path) def __eq__(self, other): if not isinstance(other, Plugin): return False if not self.key() == other.key(): return False return True def __hash__(self): return hash(self.key())
[docs]class Component(object): """An object to hold metadata for a spesfic instance of a component Holds the metadata needed to identify a instance of a component provided by a plugin Attributes: name (str): the component name provided plugin (str): the name of the providing plugin author (str): the author of the providing plugin version (Version): the verison of the providing plugin path (str): a doted name path to the component object from the top of the plugin module """ def __init__(self, name, plugin, author, version, path): """Init the component object Args: name (str): the component name provided plugin (str): the name of the providing plugin author (str): the author of the providing plugin version (Version, str): the verison of the providing plugin path (str): a doted name path to the component object from the top of the plugin module """ if not isinstance(name, basestring): raise TypeError("name must be a string component name") if not isinstance(plugin, basestring): raise TypeError("plugin must be a string plugin name") if not isinstance(author, basestring): raise TypeError("author must be a string author name") if isinstance(version, basestring): version = gen_version(version) if not isinstance(version, Version): raise TypeError("must be a SemVer Version") if not isinstance(path, basestring): raise TypeError("path must be a string path to object") self.name = name self.author = author self.plugin = plugin self.version = version self.path = path
[docs] def key(self): """returns a key to identify this component Returns: tuple: (name, plugin, author, version, path) """ return (self.name, self.plugin, self.author, self.version, self.path)
def __eq__(self, other): if not isinstance(other, Component): return False if not self.key() == other.key(): return False return True def __hash__(self): return hash(self.key())
[docs]class System(object): """A plugin system It can scan dir trees to find plugins and their provided/needed components, and with a simple load call chain load all the plugins needed. The system includes a simple event system and fires some events internal, here are their signatures: 'plugin_found': (path, plugin) path (str): the full path to the folder containing the plugin plugin (str): plugin version string (ie 'plugin_name:version') 'plugin_loaded': (plugin, plugin_required, component_needed) plugin (str): plugin version string (ie 'plugin_name:version') plugin_required (str): version string of the plugin that required the loaded plugin (version string ie 'plugin_name:version') component_needed (str): the name of the component needed by the requesting plugin 'component_loaded': (component, plugin_required, plugin_loaded) component (str): the name of the component loaded plugin_required (str, None): version string of the plugin that required the loaded component (version string ie 'plugin_name:version') (might be None) plugin_loaded (str): version string of the plugin that the component was loaded from (version string ie 'plugin_name:version') Pyitect keeps track of all the instances of the System class in `System.systems` which is a map of object id's to instances of System. Attributes: config (dict): A mapping of component names to version requirements plugins (dict): A mapping of the plugins the system knows about. Maps names to `dicts` of :class:`Version` s mapped to :class:`Plugin` config objects components (dict): A mapping of :func:`Component.key` s to loaded component objects component_map (dict): A mapping of components the system knows about. Maps names to `dicts` of :class:`Version` s mapped to :class:`Component` config objects loaded_plugins (dict): A mapping of :func:`Plugin.key` s to loaded plugin module objects enabled_plugins (list): A list of :func:`Plugin.key` s of enabled plugins using (list): A List of :func:`Component.key` s loaded by the system events (dict): A mapping of event names to lists of callable objects """ systems = [] """A list of all :class:`System` instances""" def __init__(self, config, enable_yaml=False): """Setup the system and load a configuration that may spesify plugins and versions to use for spesifc components plugins can define their own requerments the system configuration acts as a default (carefull you can break it) Args: config (dict): A mapping of component names to version requirements enable_yaml (bool): Should the system support yaml config files? """ global _have_yaml if not isinstance(config, collections.Mapping): raise PyitectError( "System configurations must be mappings of component " "names to 'plugin:version' strings") if _have_yaml and enable_yaml: self._yaml = True else: self._yaml = False self.config = config self.plugins = {} self.components = {} self.component_map = {} self.loaded_plugins = {} self.enabled_plugins = [] self.using = [] self.events = {} System.systems.append(self)
[docs] def bind_event(self, event, function): """Bind a callable object to the event name a simple event system bound to the plugin system, bind a function on an event and when the event is fired all bound functions are called with the `*args` and `**kwargs` passed to the fire call Args: event (str): name of event to bind to function (callable): Boject to be called when event fires """ if event not in self.events: self.events[event] = [] self.events[event].append(function)
[docs] def unbind_event(self, event, function): """Remove a function from an event removes the function object from the list of callables to call when event fires. does nothing if function is not bound Args: event (str): name of event bound to function (callable): object to unbind """ if event in self.events: self.events[event].remove(function)
[docs] def fire_event(self, event, *args, **kwargs): """Call all functions bound to the event name and pass all extra `*args` and `**kwargs` to the bound functions Args: event (str): name of event to fire """ if event in self.events: for function in self.events[event]: function(*args, **kwargs)
[docs] def iter_component_subtypes(self, component): """An iterater function to interate all known subtypes of a component Takes a conponent name and yeilds all known conponent names that are subtypes not including the conponent name Args: conponent (str): the conponent name to act as a base Raises: TypeError: if `component` is niether a :class:`Component` instance nor a string """ if isinstance(component, Component): component = component.name if not isinstance(component, basestring): raise TypeError( "%r object is niether a Component instance nor a string" % (component,)) for key in self.component_map: if issubcomponent(key, component) and component != key: yield key
[docs] def iter_component_providers(self, comp, subs=False, vers=False, reqs="*"): """An iterater function to interate providers of a component Takes a conponent name and yeilds providers of the conponent if `subs` is `True` yeilds providers of subtypes too if `vers` is `True` yeilds all version of the provider not just the highest `reqs` is a version requirement for the providers to meet. Defaults to any version yeilds tuples that look like: `(<component_name>, <plugin_name>, <version>)` Examples: >>> for t in iter_component_providers("foo", subs=True): print(t); ("foo", "foo_plugin", "0.1.0") ("foo", "foo_plugin2", "1.0.0") ("foo.a", "foo.a_plugin", "0.1.0") ("foo.a.b", "footastic", "10.0.0") Args: comp (str): component name to use as a base subs (bool): should subtypes be yeilded too? vers (bool): should all version be yeilded not just the highest? reqs (str, list, tuple): version spec string or list there of all items are passed to a `Spec` Raises: TypeError: if `comp` or `reqs` are passed wrong """ if isinstance(reqs, basestring): reqs = (reqs,) if not isinstance(reqs, (list, tuple)): raise TypeError( "Invalid requierment type, must be string, list, or tuple: %r" % (reqs,)) if not isinstance(comp, basestring): raise TypeError( "comp is niether a Component instance nor a string: %r" % (comp,)) spec = Spec(*reqs) if subs: comps = self.component_map.keys() else: comps = (comp,) for com in comps: if com in self.component_map and issubcomponent(com, comp): providers = self.component_map[com] for prov in providers: versions = providers[prov] if vers: for ver in sorted(versions): yield (com, prov, ver) else: yield (com, prov, spec.select(versions))
def _enable_plugin(self, plugin): # loop through and map component names to a listing of plugin names and # versions # save the plugin as enabled plugin_key = (plugin.name, plugin.version) if plugin_key not in self.enabled_plugins: self.enabled_plugins.append(plugin_key) for name, path in plugin.provides.items(): # ensure a place to list component providing plugin versions if name not in self.component_map: self.component_map[name] = {} if plugin.name not in self.component_map[name]: self.component_map[name][plugin.name] = {} if plugin.version in self.component_map[name][plugin.name]: raise PyitectDupError( "Duplicate component %s provided by plugin %s@%s" % (name, plugin.name, plugin.version)) if not path: path = name component = Component( name, plugin.name, plugin.author, plugin.version, path) self.component_map[name][plugin.name][plugin.version] = component def _enable_plugins_map(self, plugins): on_enables = [] for k in plugins: plugin = plugins[k] if not isinstance(plugin, Plugin): raise PyitectError("'%r' is not a plugin" % str(plugin)) if plugin.has_on_enable(): on_enables.append(plugin) self._enable_plugin(plugin) return on_enables def _enable_plugins_iter(self, plugins): on_enables = [] for plugin in plugins: if not isinstance(plugin, Plugin): raise PyitectError("'%r' is not a plugin" % str(plugin)) if plugin.has_on_enable(): on_enables.append(plugin) self._enable_plugin(plugin) return on_enables
[docs] def enable_plugins(self, *plugins): """Take one or more `Plugin` s and map it's components Takes a plugins metadata and remembers it's provided components so the system is awear of them Args: plugins (plugins): One or more plugins to enable. Each argument can it self be a list or map of :class:`Plugin` objects or a plain :class:`Plugin` object Raises: TypeError: If you try to pass a non :class:`Plugin` object PyitectDubError: If you try to enable a plugin that provides duplicate conponent PyitectOnEnableError: If There was an error in the on_enable PyitectLoadError: If there was an error loading a plugin to call it's on_enable """ if len(plugins) == 1: plugins = plugins[0] if isinstance(plugins, collections.Mapping): # passed a dictionary on_enables = self._enable_plugins_map(plugins) elif isinstance(plugins, collections.Iterable): # not a map but iterable on_enables = self._enable_plugins_iter(plugins) else: # single plugin plugin = plugins if not isinstance(plugin, Plugin): raise TypeError("'%r' is not a plugin" % str(plugin)) if plugin.has_on_enable(): on_enables.append(plugin) self._enable_plugin(plugin) self._run_on_enables(on_enables)
def _run_on_enables(self, *plugins): if len(plugins) == 1: plugins = plugins[0] if isinstance(plugins, Plugin): self.load_plugin( plugins.name, plugins.version, request=plugins.get_version_string() + ":on_enable") plugins.run_on_enable() elif isinstance(plugins, collections.Iterable): for plugin in plugins: self.load_plugin( plugin.name, plugin.version, request=plugin.get_version_string() + ":on_enable") plugin.run_on_enable() def _read_plugin_cfg(self, path, is_yaml=False): with open(path) as cfgfile: if (is_yaml and self._yaml): try: cfg = yaml.safe_load(cfgfile) except Exception as err: raise PyitectError( "Could not parse plugin YAML config file at %s" % (path,), cause=err) else: try: cfg = json.load(cfgfile) except Exception as err: raise PyitectError( "Could not parse plugin JSON config file at %s" % (path,), cause=err) return cfg
[docs] def add_plugin(self, path): """Adds a plugin form the provided path Args: path (str): path to a plugin folder Rasies: PyitectError: If no plugin exists at path PyitectDupError: if you try to add the same plugin twice """ exts = (".yml", ".yaml", ".json") yamls = (".yml", ".yaml") cfgpath = None is_yaml = False for ext in exts: cfgpath = os.path.join(path, os.path.basename(path) + ext) if os.path.exists(cfgpath): if ext in yamls: is_yaml = True break if cfgpath is not None and os.path.exists(cfgpath): cfg = self._read_plugin_cfg(cfgpath, is_yaml) plugin = Plugin(cfg, path) name = plugin.name version = plugin.version if name not in self.plugins: self.plugins[name] = {} if version in self.plugins[name]: raise PyitectDupError( "Duplicate plugin %s@%s at '%s'" % (name, version, path)) self.plugins[name][version] = plugin self.fire_event('plugin_found', path, plugin.get_version_string()) else: raise PyitectError("No plugin exists at %s" % (path,))
[docs] def is_plugin(self, path): """Test a path to see if it is a `Plugin` Args: path (str): path to test Returns: true if there is a plugin in the folder pointed to by path """ # a plugin exists if a file with the same name as the folder + the # .json (or .yml/.yaml if yaml is enabled) # extention exists in the folder. names = os.listdir(path) exts = [".json"] if self._yaml: exts.extend([".yml", ".yaml"]) for ext in exts: name = os.path.basename(path) + ext if name in names: return True return False
def _search_dir(self, folder): """ recursivly searches a folder for plugins """ # avoid recursion, could get nasty in a sificently big tree, also # faster. paths = [folder, ] while len(paths) > 0: # get the file names in the folder path = paths.pop(0) names = os.listdir(path) # loop through and identify plugins searching folders recursivly, # stops recursive if there is a plugin in the folder. for name in names: file = os.path.join(path, name) if os.path.isdir(file): if self.is_plugin(file): self.add_plugin(file) else: paths.append(file)
[docs] def search(self, path): """Search a path (dir or file) for a plugin in the case of a file it searches the containing dir. Args: path (str): the path to search """ # we either have a folder or a file, # if it's a file is there a plugin in the folder containing it? # if it's a folder are the plugins located somewhere within? if os.path.isdir(path): self._search_dir(path) else: self.add_plugin(os.path.dirname(path))
[docs] def resolve_highest_match(self, component, plugin, spec): """resolves the latest version of a component with requirements, takes in a component name and some requierments and gets a valid plugin name and its highest version Args: component (str): a component name plugin (str): a plugin name if it's empty we default to alphabetical order spec (Spec): a SemVer version spec Raises: TypeError: if somthing isn't the right type """ if not isinstance(component, basestring): raise TypeError( "component must be a component name string, " "got: %r" % (component,)) if not isinstance(plugin, basestring): raise TypeError( "plugin must be a plugin name string, " "got: %r" % (plugin,)) if not isinstance(spec, Spec): raise TypeError( "Version spec must be a SemVer version spec, " "got: %r" % (spec,)) if component not in self.component_map: raise PyitectNotProvidedError( "Component '%s' is not provided by any plugin", component) # if we've failed to give a requierment for somthing fill it ourselves if plugin == "": # we are gettign the first plugin name in a acending alpha-numeric # sort plugin = sorted(list(self.component_map[component].keys()))[0] if plugin not in self.component_map[component]: raise PyitectError( "Component '%s' is not provided by plugin '%s'" % (component, plugin)) versions = self.component_map[component][plugin].keys() highest_valid = spec.select(versions) if not highest_valid: raise PyitectNotMetError( "Component '%s' does not have any providers that meet " "requirements" % (component,)) return plugin, highest_valid
[docs] def load_component(self, component, plugin, version, requires=None, request=None): """Loads a component same end effect as :meth:`load` but requires an explicit name, plugin, and version. no subtypes of the component are explored no other provider is considered Args: component (str): component name to load plugin (str): plugin name to load form version (str, Version): Version to load Raises: TypeError: If things are passed worng PyitectLoadError: if there is a exception during load PyitectNotProvidedError: if the request can not be met """ # be sure not to load things twice, but besure the components is loaded # and saved if not isinstance(component, basestring): raise TypeError( "component must be a component name string, " "got: %r" % (component,)) if not isinstance(plugin, basestring): raise TypeError( "plugin must be a plugin name string, " "got: %r" % (plugin,)) if isinstance(version, basestring): version = gen_version(version) if not isinstance(version, Version): raise TypeError( "Version must be a SemVer Version, " "got: %r" % (version,)) if component not in self.component_map: raise PyitectNotProvidedError( "Component '%s' is not provided by any plugin" % (component,)) if (plugin not in self.component_map[component] or version not in self.component_map[component][plugin]): raise PyitectNotProvidedError( "Component '%s' is not provided by plugin '%s@%s'" % (component, plugin, version)) comp = self.component_map[component][plugin][version] key = comp.key() if key not in self.components: plugin_obj = self.load_plugin( plugin, version, requires=requires, request=request, comp=component) obj = plugin_obj parts = comp.path.split(".") for part in parts: if not hasattr(obj, part): raise PyitectNotProvidedError( "Plugin '%s:%s' does not have name '%s'" % (plugin, version, comp.path)) obj = getattr(obj, part) self.components[key] = obj # record the use of this component, perhaps so the users can save # the configuration self.using.append(key) self.fire_event( 'component_loaded', component, request, plugin + ":" + str(version) ) return self.components[key]
def _load_plugin_obj(self, plugin, version, requires=None, request=None, comp=None): """Loads but does not return a plugin module""" plugin_key = (plugin, version) if (plugin not in self.plugins or version not in self.plugins[plugin]): raise PyitectError( "System has no plugin '%s' at version '%s'" % (plugin, version)) cfg = self.plugins[plugin][version] # collect the imports namespace object imports = sys.modules[__name__.split('.')[0]].imports # loop through the consumed component names # load them and add them to the imports namespace reqs = {} reqs.update(cfg.consumes) if requires: reqs.update(requires) for req_name in cfg.consumes.keys(): obj = None try: obj = self.load( req_name, requires=reqs, request=cfg.get_version_string() ) except Exception as err: raise PyitectLoadError( "Could not load required component " "'%s' for plugin '%s@%s'" % (req_name, plugin, version,), cause=err) setattr(imports, req_name, obj) # load the plugin self.loaded_plugins[plugin_key] = cfg.load() # cleanup the imports namespace for req_name in cfg.consumes.keys(): delattr(imports, req_name) self.fire_event( 'plugin_loaded', cfg.get_version_string(), request, comp )
[docs] def load_plugin(self, plugin, version, requires=None, request=None, comp=None): """Takes a plugin name and version and loads it's module finds the stored Plugin object takes a Plugin object and loads the module recursively loading declared dependencies Args: plugin (str): plugin name version (str, Version): version to load requires (dict, None): a mapping of component names to version requierments to use during the load request (str, None): name of the version string of the plugin that requested a component from this plugin. `None` if not requested. comp (str): name of the component needed by teh requesting plugin. `None` if not requested. Returns: the loaded module object Raises: TypeError: if things get passed worng PyitectLoadError: if there is an exception during the load """ # we dont want to load a plugin twice just becasue it provides more # than one component, save previouly loaded plugins if isinstance(version, basestring): version = gen_version(version) if not isinstance(plugin, basestring): raise TypeError( "plugin must be a plugin name string, " "got: %r" % (plugin,)) if not isinstance(version, Version): raise TypeError( "Version must be a SemVer Version, " "got: %r" % (version,)) plugin_key = (plugin, version) if plugin_key not in self.loaded_plugins: self._load_plugin_obj(plugin, version, requires, request, comp) plugin_obj = self.loaded_plugins[plugin_key] return plugin_obj
[docs] def resolve_providers(self, component, subs=True, key=None, reverse=False): """Resolve what avalible component is used will create a lost of a component and it's subcomponents to sorted with `sorted(component key=key)` and return the first item the default, and possibly undesierable behavior, is alphabetical order of component names Args: key(func, None): a key function to sort the componet types and subtypes that are valid so you can select the correct one """ provs = sorted( self.iter_component_providers(component, subs=subs), key=key, reverse=reverse) if len(provs) < 1: raise PyitectNotProvidedError( "Component '%s' not provided by any enabled plugins" % (component,)) return provs[0]
[docs] def load(self, component, requires=None, request=None, bypass=False, subs=True, key=None, reverse=False): """Load and return a component object processes loading and returns the component by name, chain loading any required plugins to obtain dependencies. Uses the config that was provided on system creation to load correct versions, if there is a conflict throws a run time error. bypass lets the call bypass the system configuration internaly uses :meth:`iter_component_providers` to create list of viable components. basialy calls `sorted(iter_component_providers(component subs=subs), key=key)[0]` to get the component to use. Args: component (str): Name of component to load requires (dict, None): A mapping of component names to version requierments to use during the load request (str, None): The name of the requesting plugin. `None` if not requested bypass (bool): Ignore the system configured version requierments subs (bool): should sub components be considered? key (func, None): Key function to use to compaire component provider tuples from :meth:`iter_component_providers` in a `sorted` call.write the key func so that the item you want will be at index 0 reverse (bool): reverse sorting of components Returns: the loaded component object Raises: TypeError: if thigns get passed worng PyitectLoadError: if there is an exception druing load """ # set default requirements plugin = version = plugin_req = "" version_spec = Spec("*") component, plugin, version = self.resolve_providers( component, subs=subs, key=key, reverse=reverse) # merge the systems config and the passed plugin requirements (if they # were passed) to get the most relavent requirements reqs = {} if not bypass: reqs.update(self.config) if requires is not None: reqs.update(requires) # update the plugin and version requirements if they exist if component in reqs: plugin_req, version_spec = expand_version_req(reqs[component]) # get the plugin and version to load plugin, version = self.resolve_highest_match( component, plugin_req, version_spec) comp_obj = self.load_component( component, plugin, version, requires=reqs) return comp_obj
[docs] def get_plugin_module(self, plugin, version=None): """Fetch the loaded plugin module if `version` is None searches for the highest version number plugin with it's module loaded if it can't find anything it raises a runtime error Args: plugin (str): name of plugin to find version (None, str, Version): if provided load a spesfic version Returns: loaded module object Raises: TypeError: if provideing a version that is not either a `str` or a :class:`Version` PyitectError: if the Plugin can't be found PyitectLoadError: plugin module is not loaded yet """ if version: if isinstance(version, basestring): version = gen_version(version) if not isinstance(version, Version): raise TypeError( "Version must be a SemVer Version, " "got: %r" % (version,)) if plugin in self.plugins: if not version: version = sorted( self.plugins[plugin].keys(), reverse=True)[0] plugin_key = (plugin, version) if plugin_key in self.loaded_plugins: return self.loaded_plugins[plugin_key] else: raise PyitectLoadError( "Version '%s' of plugin '%s' not yet loaded" % (version, plugin)) else: raise PyitectError("Plugin '%s' not found" % (plugin,))
[docs]def expand_version_req(requires): """Take a requierment and return the Spec and the plugin name takes a requierment and pumps out a plugin name and a SemVer Spec requires is either a string of the form `("", "*", "plugin_name", plugin_name:version_spec)` or a mapping with `plugin` and `spec` keys like so `{"plugin": "plugin_name", "spec": ">=1.0.0"}` the spec key's value can be a string of comma seperated version requierments or a list of strings of the same Args: requires (str, mapping): string or mapping object with `plugin` and `spec` keys Examples: >>> expand_version_req("") ('', <Spec: (<SpecItem: * ''>,)>) >>> expand_version_req("*") ('', <Spec: (<SpecItem: * ''>,)>) >>> expand_version_req("plugin_name") ('plugin_name', <Spec: (<SpecItem: * ''>,)>) >>> expand_version_req("plugin_name:>=1.0.0") ('plugin_name', <Spec: (<SpecItem: >= Version('1.0.0')>,)>) >>> expand_version_req("plugin_name:>=1.0.0,<2.0.0") ('plugin_name', <Spec: (SpecItems... >= 1.0.0, < 2.0.0 )>) >>> expand_version_req({"plugin": "plugin_name", "spec": ">=1.0.0"}) ('plugin_name', <Spec: (<SpecItem: >= Version('1.0.0')>,)>) Raises: ValueError: when the requierment is of a bad form TypeError: when the requiers objt is not a string or mapping """ if isinstance(requires, basestring): if requires == "*" or requires == "": return ("", Spec("*")) elif ":" in requires: parts = requires.split(":") if len(parts) != 2: raise ValueError( "Version requirements strings can only contain " "at most 2 parts, " "one plugin_name and one set of version requirements, " "the parts seperated by a ':'") return (parts[0], Spec(parts[1])) else: return (requires, Spec("*")) elif isinstance(requires, collections.Mapping): if "plugin" not in requires: raise ValueError( "Version requirements mappings must contain a 'plugin' key") if "spec" not in requires: raise ValueError( "Version requirements mappings must contain a 'spec' key") return (requires["plugin"], Spec(requires["spec"])) else: raise TypeError( "Invalid type of requires object, " "must be a string or mapping object: %r" % (requires,))
[docs]def gen_version(version_str): """Generates an :class:`Version` object takes a SemVer string and returns a :class:`Version` if not a proper SemVer string it coerces it Args: version_str (str): version string to use """ try: ver = Version(version_str) except ValueError: ver = Version.coerce(version_str) return ver
[docs]def get_unique_name(*parts): """Generate a fixed lenght unique name from parts takes the parts turns them into strings and uses them in a sha1 hash used internaly to ensure module object for plugins have unique names like so `get_unique_name(plugin.author, plugin.get_version_string())` Returns: str: name hash """ def _str_encode(obj): # ensure bytes is there in Python2 if PY2: return str(obj) else: return str(obj).encode() name_hash = hashlib.sha1() for part in parts: name_hash.update(_str_encode(part)) return str(name_hash.hexdigest())
[docs]def issubcomponent(comp1, comp2): """Check if comp1 is a subtype of comp2 Returns whether the Component passed as comp1 validates as a subtype of the Component passed as comp2. if strings are passed as either peramater they are treated as Component names. if a Component instance is passed it's `name` property is pulled. Args: comp1 (str, Component): The Component or component name to check comp2 (str, Component): The Component or component name to compair to """ if isinstance(comp1, Component): comp1 = comp1.name if isinstance(comp2, Component): comp2 = comp2.name if not isinstance(comp1, basestring): raise TypeError("comp1 must either be a string name or Component") if not isinstance(comp2, basestring): raise TypeError("comp2 must either be a string name or Component") comp1_parts = comp1.split(".") comp2_parts = comp2.split(".") if len(comp1_parts) < len(comp2_parts): return False if not tuple(comp1_parts[:len(comp2_parts)]) == tuple(comp2_parts): return False return True
[docs]class PyitectError(Exception): """ Wraps Exceptions for Chained trace backs As Pyitect is intended for use across pyhton 2 and 3 a way was needed to Ensure that exceptions caused during the import of plugin modules tell *why* that import failed insed of just `failed to import 'bla'` This code is a modifed version of a `CausedException` class posed to ActiveState back in Sep. 2012 Licensed under MIT license the ability handle trees of exceptions was removed http://code.activestate.com/recipes/578252-python-exception-chains-or-trees/?in=user-4182236 """ def __init__(self, *args, **kwargs): if len(args) == 1 and not kwargs and isinstance(args[0], Exception): # we shall just wrap a non-caused exception self.stack = ( traceback.format_stack()[:-2] + traceback.format_tb(sys.exc_info()[2])) # ^^^ let's hope the information is still there; caller must take # care of this. self.wrapped = args[0] self.cause = None super(PyitectError, self).__init__(repr(args[0])) return self.wrapped = None self.stack = traceback.format_stack()[:-1] # cut off current frame try: cause = kwargs['cause'] del kwargs['cause'] except: cause = None self.cause = cause super(PyitectError, self).__init__(*args, **kwargs) def causeChain(self, indentation=' ', alreadyMentionedTree=[]): yield "Traceback (most recent call last):\n" ellipsed = 0 for i, line in enumerate(self.stack): if (ellipsed is not False and i < len(alreadyMentionedTree) and line == alreadyMentionedTree[i]): ellipsed += 1 else: if ellipsed: yield " ... (%d frame%s repeated)\n" % ( ellipsed, "" if ellipsed == 1 else "s") ellipsed = False # marker for "given out" yield line exc = self if self.wrapped is None else self.wrapped for line in traceback.format_exception_only(exc.__class__, exc): yield line if self.cause: yield "caused by: %s\n" % (self.cause,) for line in self.cause.causeChain(indentation, self.stack): yield re.sub(r'([^\n]*\n)', indentation + r'\1', line) def write(self, stream=None, indentation=' '): stream = sys.stderr if stream is None else stream for line in self.causeChain(indentation): stream.write(line)
[docs]class PyitectNotProvidedError(PyitectError): """Raised if a conponent is not provided""" def __init__(self, *args, **kwargs): super(PyitectNotProvidedError, self).__init__(*args, **kwargs)
[docs]class PyitectNotMetError(PyitectError): """Raised if requierments are not met""" def __init__(self, *args, **kwargs): super(PyitectNotMetError, self).__init__(*args, **kwargs)
[docs]class PyitectLoadError(PyitectError): """Raises if a plugins module is not yet loaded or fais to load""" def __init__(self, *args, **kwargs): super(PyitectLoadError, self).__init__(*args, **kwargs)
[docs]class PyitectOnEnableError(PyitectError): """Raised if and on_enable call failes""" def __init__(self, *args, **kwargs): super(PyitectOnEnableError, self).__init__(*args, **kwargs)
[docs]class PyitectDupError(PyitectError): """ Raised if you try to add a duplicate plugin or duplicate component provider """ def __init__(self, *args, **kwargs): super(PyitectDupError, self).__init__(*args, **kwargs)