summaryrefslogtreecommitdiff
path: root/python/helpers/pydev/third_party/pluginbase.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/helpers/pydev/third_party/pluginbase.py')
-rw-r--r--python/helpers/pydev/third_party/pluginbase.py454
1 files changed, 454 insertions, 0 deletions
diff --git a/python/helpers/pydev/third_party/pluginbase.py b/python/helpers/pydev/third_party/pluginbase.py
new file mode 100644
index 000000000000..0ad6404eee00
--- /dev/null
+++ b/python/helpers/pydev/third_party/pluginbase.py
@@ -0,0 +1,454 @@
+# -*- coding: utf-8 -*-
+"""
+ pluginbase
+ ~~~~~~~~~~
+
+ Pluginbase is a module for Python that provides a system for building
+ plugin based applications.
+
+ :copyright: (c) Copyright 2014 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+import os
+import sys
+
+from pydevd_constants import IS_PY24, IS_PY3K
+
+
+if IS_PY24:
+ from third_party.uuid_old import uuid4
+else:
+ from uuid import uuid4
+
+if IS_PY3K:
+ import pkgutil
+else:
+ import pkgutil_old as pkgutil
+
+import errno
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+import threading
+
+from types import ModuleType
+from weakref import ref as weakref
+
+
+PY2 = sys.version_info[0] == 2
+if PY2:
+ text_type = unicode
+ string_types = (unicode, str)
+ from cStringIO import StringIO as NativeBytesIO
+else:
+ text_type = str
+ string_types = (str,)
+ from io import BytesIO as NativeBytesIO
+
+
+_local = threading.local()
+
+_internalspace = ModuleType(__name__ + '._internalspace')
+_internalspace.__path__ = []
+sys.modules[_internalspace.__name__] = _internalspace
+
+
+def get_plugin_source(module=None, stacklevel=None):
+ """Returns the :class:`PluginSource` for the current module or the given
+ module. The module can be provided by name (in which case an import
+ will be attempted) or as a module object.
+
+ If no plugin source can be discovered, the return value from this method
+ is `None`.
+
+ This function can be very useful if additional data has been attached
+ to the plugin source. For instance this could allow plugins to get
+ access to a back reference to the application that created them.
+
+ :param module: optionally the module to locate the plugin source of.
+ :param stacklevel: defines how many levels up the module should search
+ for before it discovers the plugin frame. The
+ default is 0. This can be useful for writing wrappers
+ around this function.
+ """
+ if module is None:
+ frm = sys._getframe((stacklevel or 0) + 1)
+ name = frm.f_globals['__name__']
+ glob = frm.f_globals
+ elif isinstance(module, string_types):
+ frm = sys._getframe(1)
+ name = module
+ glob = __import__(module, frm.f_globals,
+ frm.f_locals, ['__dict__']).__dict__
+ else:
+ name = module.__name__
+ glob = module.__dict__
+ return _discover_space(name, glob)
+
+
+def _discover_space(name, globals):
+ try:
+ return _local.space_stack[-1]
+ except (AttributeError, IndexError):
+ pass
+
+ if '__pluginbase_state__' in globals:
+ return globals['__pluginbase_state__'].source
+
+ mod_name = globals.get('__name__')
+ if mod_name is not None and \
+ mod_name.startswith(_internalspace.__name__ + '.'):
+ end = mod_name.find('.', len(_internalspace.__name__) + 1)
+ space = sys.modules.get(mod_name[:end])
+ if space is not None:
+ return space.__pluginbase_state__.source
+
+
+def _shutdown_module(mod):
+ members = list(mod.__dict__.items())
+ for key, value in members:
+ if key[:1] != '_':
+ setattr(mod, key, None)
+ for key, value in members:
+ setattr(mod, key, None)
+
+
+def _to_bytes(s):
+ if isinstance(s, text_type):
+ return s.encode('utf-8')
+ return s
+
+
+class _IntentionallyEmptyModule(ModuleType):
+
+ def __getattr__(self, name):
+ try:
+ return ModuleType.__getattr__(self, name)
+ except AttributeError:
+ if name[:2] == '__':
+ raise
+ raise RuntimeError(
+ 'Attempted to import from a plugin base module (%s) without '
+ 'having a plugin source activated. To solve this error '
+ 'you have to move the import into a "with" block of the '
+ 'associated plugin source.' % self.__name__)
+
+
+class _PluginSourceModule(ModuleType):
+
+ def __init__(self, source):
+ modname = '%s.%s' % (_internalspace.__name__, source.spaceid)
+ ModuleType.__init__(self, modname)
+ self.__pluginbase_state__ = PluginBaseState(source)
+
+ @property
+ def __path__(self):
+ try:
+ ps = self.__pluginbase_state__.source
+ except AttributeError:
+ return []
+ return ps.searchpath + ps.base.searchpath
+
+
+def _setup_base_package(module_name):
+ try:
+ mod = __import__(module_name, None, None, ['__name__'])
+ except ImportError:
+ mod = None
+ if '.' in module_name:
+ parent_mod = __import__(module_name.rsplit('.', 1)[0],
+ None, None, ['__name__'])
+ else:
+ parent_mod = None
+
+ if mod is None:
+ mod = _IntentionallyEmptyModule(module_name)
+ if parent_mod is not None:
+ setattr(parent_mod, module_name.rsplit('.', 1)[-1], mod)
+ sys.modules[module_name] = mod
+
+
+class PluginBase(object):
+ """The plugin base acts as a control object around a dummy Python
+ package that acts as a container for plugins. Usually each
+ application creates exactly one base object for all plugins.
+
+ :param package: the name of the package that acts as the plugin base.
+ Usually this module does not exist. Unless you know
+ what you are doing you should not create this module
+ on the file system.
+ :param searchpath: optionally a shared search path for modules that
+ will be used by all plugin sources registered.
+ """
+
+ def __init__(self, package, searchpath=None):
+ #: the name of the dummy package.
+ self.package = package
+ if searchpath is None:
+ searchpath = []
+ #: the default search path shared by all plugins as list.
+ self.searchpath = searchpath
+ _setup_base_package(package)
+
+ def make_plugin_source(self, *args, **kwargs):
+ """Creats a plugin source for this plugin base and returns it.
+ All parameters are forwarded to :class:`PluginSource`.
+ """
+ return PluginSource(self, *args, **kwargs)
+
+
+class PluginSource(object):
+ """The plugin source is what ultimately decides where plugins are
+ loaded from. Plugin bases can have multiple plugin sources which act
+ as isolation layer. While this is not a security system it generally
+ is not possible for plugins from different sources to accidentally
+ cross talk.
+
+ Once a plugin source has been created it can be used in a ``with``
+ statement to change the behavior of the ``import`` statement in the
+ block to define which source to load the plugins from::
+
+ plugin_source = plugin_base.make_plugin_source(
+ searchpath=['./path/to/plugins', './path/to/more/plugins'])
+
+ with plugin_source:
+ from myapplication.plugins import my_plugin
+
+ :param base: the base this plugin source belongs to.
+ :param identifier: optionally a stable identifier. If it's not defined
+ a random identifier is picked. It's useful to set this
+ to a stable value to have consistent tracebacks
+ between restarts and to support pickle.
+ :param searchpath: a list of paths where plugins are looked for.
+ :param persist: optionally this can be set to `True` and the plugins
+ will not be cleaned up when the plugin source gets
+ garbage collected.
+ """
+ # Set these here to false by default so that a completely failing
+ # constructor does not fuck up the destructor.
+ persist = False
+ mod = None
+
+ def __init__(self, base, identifier=None, searchpath=None,
+ persist=False):
+ #: indicates if this plugin source persists or not.
+ self.persist = persist
+ if identifier is None:
+ identifier = str(uuid4())
+ #: the identifier for this source.
+ self.identifier = identifier
+ #: A reference to the plugin base that created this source.
+ self.base = base
+ #: a list of paths where plugins are searched in.
+ self.searchpath = searchpath
+ #: The internal module name of the plugin source as it appears
+ #: in the :mod:`pluginsource._internalspace`.
+ div = None
+ self.spaceid = '_sp' + md5(
+ _to_bytes(self.base.package) + _to_bytes('|') +
+ _to_bytes(self.identifier)
+ ).hexdigest()
+ #: a reference to the module on the internal
+ #: :mod:`pluginsource._internalspace`.
+ self.mod = _PluginSourceModule(self)
+
+ if hasattr(_internalspace, self.spaceid):
+ raise RuntimeError('This plugin source already exists.')
+ sys.modules[self.mod.__name__] = self.mod
+ setattr(_internalspace, self.spaceid, self.mod)
+
+ def __del__(self):
+ if not self.persist:
+ self.cleanup()
+
+ def list_plugins(self):
+ """Returns a sorted list of all plugins that are available in this
+ plugin source. This can be useful to automatically discover plugins
+ that are available and is usually used together with
+ :meth:`load_plugin`.
+ """
+ rv = []
+ for _, modname, ispkg in pkgutil.iter_modules(self.mod.__path__):
+ rv.append(modname)
+ return sorted(rv)
+
+ def load_plugin(self, name):
+ """This automatically loads a plugin by the given name from the
+ current source and returns the module. This is a convenient
+ alternative to the import statement and saves you from invoking
+ ``__import__`` or a similar function yourself.
+
+ :param name: the name of the plugin to load.
+ """
+ if '.' in name:
+ raise ImportError('Plugin names cannot contain dots.')
+
+ #with self:
+ # return __import__(self.base.package + '.' + name,
+ # globals(), {}, ['__name__'])
+
+ self.__assert_not_cleaned_up()
+ _local.__dict__.setdefault('space_stack', []).append(self)
+ try:
+ res = __import__(self.base.package + '.' + name,
+ globals(), {}, ['__name__'])
+ return res
+ finally:
+ try:
+ _local.space_stack.pop()
+ except (AttributeError, IndexError):
+ pass
+
+ def open_resource(self, plugin, filename):
+ """This function locates a resource inside the plugin and returns
+ a byte stream to the contents of it. If the resource cannot be
+ loaded an :exc:`IOError` will be raised. Only plugins that are
+ real Python packages can contain resources. Plain old Python
+ modules do not allow this for obvious reasons.
+
+ .. versionadded:: 0.3
+
+ :param plugin: the name of the plugin to open the resource of.
+ :param filename: the name of the file within the plugin to open.
+ """
+ mod = self.load_plugin(plugin)
+ fn = getattr(mod, '__file__', None)
+ if fn is not None:
+ if fn.endswith(('.pyc', '.pyo')):
+ fn = fn[:-1]
+ if os.path.isfile(fn):
+ return open(os.path.join(os.path.dirname(fn), filename), 'rb')
+ buf = pkgutil.get_data(self.mod.__name__ + '.' + plugin, filename)
+ if buf is None:
+ raise IOError(errno.ENOEXITS, 'Could not find resource')
+ return NativeBytesIO(buf)
+
+ def cleanup(self):
+ """Cleans up all loaded plugins manually. This is necessary to
+ call only if :attr:`persist` is enabled. Otherwise this happens
+ automatically when the source gets garbage collected.
+ """
+ self.__cleanup()
+
+ def __cleanup(self, _sys=sys, _shutdown_module=_shutdown_module):
+ # The default parameters are necessary because this can be fired
+ # from the destructor and so late when the interpreter shuts down
+ # that these functions and modules might be gone.
+ if self.mod is None:
+ return
+ modname = self.mod.__name__
+ self.mod.__pluginbase_state__ = None
+ self.mod = None
+ try:
+ delattr(_internalspace, self.spaceid)
+ except AttributeError:
+ pass
+ prefix = modname + '.'
+ _sys.modules.pop(modname)
+ for key, value in list(_sys.modules.items()):
+ if not key.startswith(prefix):
+ continue
+ mod = _sys.modules.pop(key, None)
+ if mod is None:
+ continue
+ _shutdown_module(mod)
+
+ def __assert_not_cleaned_up(self):
+ if self.mod is None:
+ raise RuntimeError('The plugin source was already cleaned up.')
+
+ def __enter__(self):
+ self.__assert_not_cleaned_up()
+ _local.__dict__.setdefault('space_stack', []).append(self)
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ try:
+ _local.space_stack.pop()
+ except (AttributeError, IndexError):
+ pass
+
+ def _rewrite_module_path(self, modname):
+ self.__assert_not_cleaned_up()
+ if modname == self.base.package:
+ return self.mod.__name__
+ elif modname.startswith(self.base.package + '.'):
+ pieces = modname.split('.')
+ return self.mod.__name__ + '.' + '.'.join(
+ pieces[self.base.package.count('.') + 1:])
+
+
+class PluginBaseState(object):
+ __slots__ = ('_source',)
+
+ def __init__(self, source):
+ if source.persist:
+ self._source = lambda: source
+ else:
+ self._source = weakref(source)
+
+ @property
+ def source(self):
+ rv = self._source()
+ if rv is None:
+ raise AttributeError('Plugin source went away')
+ return rv
+
+
+class _ImportHook(ModuleType):
+
+ def __init__(self, name, system_import):
+ ModuleType.__init__(self, name)
+ self._system_import = system_import
+ self.enabled = True
+
+ def enable(self):
+ """Enables the import hook which drives the plugin base system.
+ This is the default.
+ """
+ self.enabled = True
+
+ def disable(self):
+ """Disables the import hook and restores the default import system
+ behavior. This effectively breaks pluginbase but can be useful
+ for testing purposes.
+ """
+ self.enabled = False
+
+ def plugin_import(self, name, globals=None, locals=None,
+ fromlist=None, level=-2):
+ import_name = name
+ if self.enabled:
+ ref_globals = globals
+ if ref_globals is None:
+ ref_globals = sys._getframe(1).f_globals
+ space = _discover_space(name, ref_globals)
+ if space is not None:
+ actual_name = space._rewrite_module_path(name)
+ if actual_name is not None:
+ import_name = actual_name
+ if level == -2:
+ # fake impossible value; default value depends on version
+ if IS_PY24:
+ # the level parameter was added in version 2.5
+ return self._system_import(import_name, globals, locals, fromlist)
+ elif IS_PY3K:
+ # default value for level parameter in python 3
+ level = 0
+ else:
+ # default value for level parameter in other versions
+ level = -1
+ return self._system_import(import_name, globals, locals,
+ fromlist, level)
+
+
+try:
+ import __builtin__ as builtins
+except ImportError:
+ import builtins
+
+import_hook = _ImportHook(__name__ + '.import_hook', builtins.__import__)
+builtins.__import__ = import_hook.plugin_import
+sys.modules[import_hook.__name__] = import_hook
+del builtins