diff options
Diffstat (limited to 'pyload/utils/PluginLoader.py')
-rw-r--r-- | pyload/utils/PluginLoader.py | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/pyload/utils/PluginLoader.py b/pyload/utils/PluginLoader.py new file mode 100644 index 000000000..038ac9b23 --- /dev/null +++ b/pyload/utils/PluginLoader.py @@ -0,0 +1,333 @@ +############################################################################### +# Copyright(c) 2008-2013 pyLoad Team +# http://www.pyload.org +# +# This file is part of pyLoad. +# pyLoad is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Subjected to the terms and conditions in LICENSE +# +# @author: RaNaN +############################################################################### + +import re + +from os import listdir, makedirs +from os.path import isfile, join, exists, basename +from sys import version_info +from time import time +from collections import defaultdict +from logging import getLogger + +from pyload.lib.SafeEval import const_eval as literal_eval +from pyload.plugins.Base import Base + +from new_collections import namedtuple + +PluginTuple = namedtuple("PluginTuple", "version re deps category user path") + + +class BaseAttributes(defaultdict): + """ Dictionary that loads defaults values from Base object """ + + def __missing__(self, key): + attr = "__%s__" % key + if not hasattr(Base, attr): + return defaultdict.__missing__(self, key) + + return getattr(Base, attr) + +class LoaderFactory: + """ Container for multiple plugin loaders """ + + def __init__(self, *loader): + self.loader = list(loader) + + def __iter__(self): + return self.loader.__iter__() + + + def checkVersions(self): + """ Reduces every plugin loader to the globally newest version. + Afterwards every plugin is unique across all available loader """ + for plugin_type in self.loader[0].iterTypes(): + for loader in self.loader: + # iterate all plugins + for plugin, info in loader.getPlugins(plugin_type).iteritems(): + # now iterate all other loaders + for l2 in self.loader: + if l2 is not loader: + l2.removePlugin(plugin_type, plugin, info.version) + + def getPlugin(self, plugin, name): + """ retrieve a plugin from an available loader """ + for loader in self.loader: + if loader.hasPlugin(plugin, name): + return loader.getPlugin(plugin, name) + + +class PluginLoader: + """ + Class to provide and load plugins from the file-system + """ + TYPES = ("crypter", "hoster", "accounts", "addons", "network", "internal") + + BUILTIN = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*(True|False|None|[0-9x.]+)', re.I) + SINGLE = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*(?:r|u|_)?((?:(?<!")"(?!")|\').*(?:(?<!")"(?!")|\'))', + re.I) + # finds the beginning of a expression that could span multiple lines + MULTI = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*(\(|\{|\[|"{3})',re.I) + + # closing symbols + MULTI_MATCH = { + "{": "}", + "(": ")", + "[": "]", + '"""': '"""' + } + + NO_MATCH = re.compile(r'^no_match$') + + def __init__(self, path, package, config): + self.path = path + self.package = package + self.config = config + self.log = getLogger("log") + self.plugins = {} + + self.createIndex() + + def logDebug(self, plugin, name, msg): + self.log.debug("Plugin %s | %s: %s" % (plugin, name, msg)) + + def createIndex(self): + """create information for all plugins available""" + + if not exists(self.path): + makedirs(self.path) + if not exists(join(self.path, "__init__.py")): + f = open(join(self.path, "__init__.py"), "wb") + f.close() + + a = time() + for plugin in self.TYPES: + self.plugins[plugin] = self.parse(plugin) + + self.log.debug("Created index of plugins for %s in %.2f ms", self.path, (time() - a) * 1000) + + def parse(self, folder): + """ Analyze and parses all plugins in folder """ + plugins = {} + pfolder = join(self.path, folder) + if not exists(pfolder): + makedirs(pfolder) + if not exists(join(pfolder, "__init__.py")): + f = open(join(pfolder, "__init__.py"), "wb") + f.close() + + for f in listdir(pfolder): + if (isfile(join(pfolder, f)) and f.endswith(".py") or f.endswith("_25.pyc") or f.endswith( + "_26.pyc") or f.endswith("_27.pyc")) and not f.startswith("_"): + if f.endswith("_25.pyc") and version_info[0:2] != (2, 5): + continue + elif f.endswith("_26.pyc") and version_info[0:2] != (2, 6): + continue + elif f.endswith("_27.pyc") and version_info[0:2] != (2, 7): + continue + + # replace suffix and version tag + name = f[:-3] + if name[-1] == ".": name = name[:-4] + + plugin = self.parsePlugin(join(pfolder, f), folder, name) + if plugin: + plugins[name] = plugin + + return plugins + + def parseAttributes(self, filename, name, folder=""): + """ Parse attribute dict from plugin""" + data = open(filename, "rb") + content = data.read() + data.close() + + attrs = BaseAttributes() + for m in self.BUILTIN.findall(content) + self.SINGLE.findall(content) + self.parseMultiLine(content): + #replace gettext function and eval result + try: + attrs[m[0]] = literal_eval(m[-1].replace("_(", "(")) + except Exception, e: + self.logDebug(folder, name, "Error when parsing: %s" % m[-1]) + self.log.debug(str(e)) + + if not hasattr(Base, "__%s__" % m[0]): + if m[0] != "type": #TODO remove type from all plugins, its not needed + self.logDebug(folder, name, "Unknown attribute '%s'" % m[0]) + + return attrs + + def parseMultiLine(self, content): + # regexp is not enough to parse multi line statements + attrs = [] + for m in self.MULTI.finditer(content): + attr = m.group(1) + char = m.group(2) + # the end char to search for + endchar = self.MULTI_MATCH[char] + size = len(endchar) + # save number of of occurred + stack = 0 + endpos = m.start(2) - size + for i in xrange(m.end(2), len(content) - size + 1): + if content[i:i+size] == endchar: + # closing char seen and match now complete + if stack == 0: + endpos = i + break + else: + stack -= 1 + elif content[i:i+size] == char: + stack += 1 + + # in case the end was not found match will be empty + attrs.append((attr, content[m.start(2): endpos + size])) + + return attrs + + + def parsePlugin(self, filename, folder, name): + """ Parses a plugin from disk, folder means plugin type in this context. Also sets config. + + :arg home: dict with plugins, of which the found one will be matched against (according version) + :returns PluginTuple""" + + attrs = self.parseAttributes(filename, name, folder) + if not attrs: return + + version = 0 + if "version" in attrs: + try: + version = float(attrs["version"]) + except ValueError: + self.logDebug(folder, name, "Invalid version %s" % attrs["version"]) + version = 9 #TODO remove when plugins are fixed, causing update loops + else: + self.logDebug(folder, name, "No version attribute") + + if "pattern" in attrs and attrs["pattern"]: + try: + plugin_re = re.compile(attrs["pattern"], re.I) + except: + self.logDebug(folder, name, "Invalid regexp pattern '%s'" % attrs["pattern"]) + plugin_re = self.NO_MATCH + else: + plugin_re = self.NO_MATCH + + deps = attrs["dependencies"] + category = attrs["category"] if folder == "addons" else "" + + # create plugin tuple + # user_context=True is the default for non addons plugins + plugin = PluginTuple(version, plugin_re, deps, category, + bool(folder != "addons" or attrs["user_context"]), filename) + + # These have none or their own config + if folder in ("internal", "accounts", "network"): + return plugin + + if folder == "addons" and "config" not in attrs and not attrs["internal"]: + attrs["config"] = (["activated", "bool", "Activated", False],) + + if "config" in attrs and attrs["config"] is not None: + config = attrs["config"] + desc = attrs["description"] + expl = attrs["explanation"] + + # Convert tuples to list + config = [list(x) for x in config] + + if folder == "addons" and not attrs["internal"]: + for item in config: + if item[0] == "activated": break + else: # activated flag missing + config.insert(0, ("activated", "bool", "Activated", False)) + + try: + self.config.addConfigSection(name, name, desc, expl, config) + except: + self.logDebug(folder, name, "Invalid config %s" % config) + + return plugin + + def iterPlugins(self): + """ Iterates over all plugins returning (type, name, info) with info as PluginTuple """ + + for plugin, data in self.plugins.iteritems(): + for name, info in data.iteritems(): + yield plugin, name, info + + def iterTypes(self): + """ Iterate over the available plugin types """ + + for plugin in self.plugins.iterkeys(): + yield plugin + + def hasPlugin(self, plugin, name): + """ Check if certain plugin is available """ + return plugin in self.plugins and name in self.plugins[plugin] + + def getPlugin(self, plugin, name): + """ Return plugin info for a single entity """ + try: + return self.plugins[plugin][name] + except KeyError: + return None + + def getPlugins(self, plugin): + """ Return all plugins of given plugin type """ + return self.plugins[plugin] + + def removePlugin(self, plugin, name, available_version=None): + """ Removes a plugin from the index. + Optionally only when its version is below or equal the available one + """ + try: + if available_version is not None: + if self.plugins[plugin][name] <= available_version: + del self.plugins[plugin][name] + else: + del self.plugins[plugin][name] + + # no errors are thrown if the plugin didn't existed + except KeyError: + return + + def isUserPlugin(self, name): + """ Determine if given plugin name is enable for user_context in any plugin type """ + for plugins in self.plugins: + if name in plugins and name[plugins].user: + return True + + return False + + def savePlugin(self, content): + """ Saves a plugin to disk """ + + def loadModule(self, plugin, name): + """ Returns loaded module for plugin + + :param plugin: plugin type, subfolder of module.plugins + :raises Exception: Everything could go wrong, failures needs to be catched + """ + plugins = self.plugins[plugin] + # convert path to python recognizable import + path = basename(plugins[name].path).replace(".pyc", "").replace(".py", "") + module = __import__(self.package + ".%s.%s" % (plugin, path), globals(), locals(), path) + return module + + def loadAttributes(self, plugin, name): + """ Same as `parseAttributes` for already indexed plugins """ + return self.parseAttributes(self.plugins[plugin][name].path, name, plugin)
\ No newline at end of file |