diff --git a/requirements.txt b/requirements.txt index 27fbf8992..f8e768b38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools chardet pytz python-dateutil diff --git a/src/plugin.py b/src/plugin.py index 63555090a..3f9723709 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -34,23 +34,42 @@ import os.path import linecache import importlib.util +try: + import pkg_resources +except ImportError: + pkg_resources = None + if not hasattr(importlib.util, 'module_from_spec'): # Python < 3.5 import imp from . import callbacks, conf, log, registry +ENTRYPOINT_GROUPS = [ + 'limnoria.plugins', +] + installDir = os.path.dirname(sys.modules[__name__].__file__) _pluginsDir = os.path.join(installDir, 'plugins') class Deprecated(ImportError): pass +def loadPluginFromEntrypoint(name): + if pkg_resources: + for entrypoint_group in ENTRYPOINT_GROUPS: + for entrypoint in pkg_resources.iter_entry_points(entrypoint_group): + if entrypoint.name.lower() == name.lower(): + return entrypoint.load() + + return None + def loadPluginModule(name, ignoreDeprecation=False): """Loads (and returns) the module for the plugin with the given name.""" files = [] pluginDirs = conf.supybot.directories.plugins()[:] pluginDirs.append(_pluginsDir) + module = None for dir in pluginDirs: try: files.extend(os.listdir(dir)) @@ -63,31 +82,36 @@ def loadPluginModule(name, ignoreDeprecation=False): if len(matched_names) >= 1: name = matched_names[0] else: - raise ImportError(name) - - try: - if hasattr(importlib.util, 'module_from_spec'): - # Python >= 3.5 - spec = importlib.machinery.PathFinder.find_spec(name, pluginDirs) - if spec is None or spec.loader is None: - # spec is None if 'name' can't be found; and - # spec.loader might be None in some rare occasions as well - # (eg. for namespace packages) + module = loadPluginFromEntrypoint(name) + if module is None: raise ImportError(name) - module = importlib.util.module_from_spec(spec) - sys.modules[module.__name__] = module - spec.loader.exec_module(module) - else: - # Python < 3.5 - moduleInfo = imp.find_module(name, pluginDirs) - module = imp.load_module(name, *moduleInfo) - except: - sys.modules.pop(name, None) - keys = list(sys.modules.keys()) - for key in keys: - if key.startswith(name + '.'): - sys.modules.pop(key) - raise + + if module is None: + # Found by listing files; must now import it + try: + if hasattr(importlib.util, 'module_from_spec'): + # Python >= 3.5 + spec = importlib.machinery.PathFinder.find_spec(name, pluginDirs) + if spec is None or spec.loader is None: + # spec is None if 'name' can't be found; and + # spec.loader might be None in some rare occasions as well + # (eg. for namespace packages) + raise ImportError(name) + module = importlib.util.module_from_spec(spec) + sys.modules[module.__name__] = module + spec.loader.exec_module(module) + else: + # Python < 3.5 + moduleInfo = imp.find_module(name, pluginDirs) + module = imp.load_module(name, *moduleInfo) + except: + sys.modules.pop(name, None) + keys = list(sys.modules.keys()) + for key in keys: + if key.startswith(name + '.'): + sys.modules.pop(key) + raise + if 'deprecated' in module.__dict__ and module.deprecated: if ignoreDeprecation: log.warning('Deprecated plugin loaded: %s', name) diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 000000000..8e6581ea9 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,99 @@ +### +# Copyright (c) 2020, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import os +import sys + +try: + import setuptools +except ImportError: + setuptools = None + +from . import authors + +if setuptools: + def plugin_setup(plugin, **kwargs): + """Wrapper of setuptools.setup that auto-fills some fields for + Limnoria plugins.""" + if isinstance(plugin, str): + if plugin in sys.modules: + plugin = sys.modules[plugin] + else: + setup_path = sys.modules['__main__'].__file__ + sys.path.insert(0, os.path.join(os.path.dirname(setup_path), '..')) + plugin = __import__(plugin) + + author = plugin.__author__ + version = plugin.__version__ + url = plugin.__url__ + maintainer = getattr(plugin, '__maintainer__', authors.unknown) + + kwargs.setdefault('package_data', {}).setdefault('', []).append('*.po') + + capitalized_name = plugin.Class.__name__ + kwargs.setdefault( + 'name', 'limnoria-%s' % capitalized_name.lower()) + if version: + kwargs.setdefault('version', version) + if url: + kwargs.setdefault('url', url) + + module_name = kwargs['name'].replace('-', '_') + kwargs.setdefault('packages', [module_name]) + kwargs.setdefault('package_dir', {module_name: '.'}) + kwargs.setdefault('entry_points', { + 'limnoria.plugins': '%s = %s' % (capitalized_name, module_name)}) + + kwargs.setdefault('install_requires', []).append('limnoria') + + kwargs.setdefault('classifiers', []).extend([ + 'Environment :: Plugins', + 'Programming Language :: Python :: 3', + 'Topic :: Communications :: Chat', + ]) + + if author is not authors.unknown: + if author.name or author.nick: + kwargs.setdefault('author', author.name or author.nick) + if author.email: + kwargs.setdefault('author_email', author.email) + + if maintainer is not authors.unknown: + if maintainer.name or maintainer.nick: + kwargs.setdefault( + 'maintainer', maintainer.name or maintainer.nick) + if maintainer.email: + kwargs.setdefault('maintainer_email', maintainer.email) + + setuptools.setup( + **kwargs) + +else: + def plugin_setup(plugin, **kwargs): + raise ImportError('setuptools')