#!/usr/bin/env python3
# The MIT License (MIT)
#
# Copyright (c) 2014-2018 <see AUTHORS.txt>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# Modules
# ------------------------------------------------
# std
import os
import logging
import shutil
import warnings
# third party
import blinker
# emsm
from . import argparse_
from .lib import userinput
from .worlds import WorldWrapper
# Data
# ------------------------------------------------
__all__ = [
"BasePlugin"
]
log = logging.getLogger(__file__)
# Classes
# ------------------------------------------------
[docs]class BasePlugin(object):
"""
This is the base class for all plugins.
If you want to know, how to implement your own plugin, you should also
take a look at the :mod:`plugins.hellodolly` plugin.
"""
#: Integer with the init priority of the plugin.
#: A higher value results in a later initialisation.
INIT_PRIORITY = 0
#: Integer with the finish priority of the plugin.
#: A higher value results in a later call of the finish method.
FINISH_PRIORITY = 0
#: The last version number of the EMSM version that worked correctly
#: with that plugin.
VERSION = "0.0.0"
#: The plugin package can be downloaded from this url.
#:
#: .. seealso::
#:
#: * :mod:`emsm.plugins.plugins` package manager
DOWNLOAD_URL = str()
#: This string is displayed when the ``--long-help`` argument is used.
DESCRIPTION = str()
#: If ``True``, the plugin has no :meth:`argparser` and can therefore
#: not be invoked from the command line.
HIDDEN = False
#: Signal, that is emitted, when a plugin has been uninstalled.
plugin_uninstalled = blinker.signal("plugin_uninstalled")
def __init__(self, app, name):
"""
Initialises the configuration and the storage of the plugin.
**Override:**
* Extend, but do not override.
"""
log.info("initialising '{}' ...".format(name))
self.__app = app
self.__name = name
# Get the argparser for this plugin and set it up.
if type(self).HIDDEN:
self.__argparser = None
else:
self.__argparser = app.argparser().plugin_parser(name)
self.__argparser.add_argument(
"--long-help",
action = argparse_.LongHelpAction,
description = type(self).DESCRIPTION
)
return None
[docs] def app(self):
"""
Returns the parent EMSM :class:`~emsm.application.Application`
that owns this plugin.
"""
return self.__app
[docs] def name(self):
"""
Returns the name of the plugin.
"""
return self.__name
[docs] def conf(self):
"""
Returns a dictionary like object that contains the configuration
of the plugin.
.. deprecated:: 4.0.16-beta
This method has been replaced by :meth:`global_conf` to clarify
the difference to :meth:`world_conf`.
"""
msg = "The BasePlugin.conf() method has been marked as deprecated. "\
"Please use BasePlugin.global_conf() instead."
warnings.warn(msg, DeprecationWarning)
return self.global_conf()
[docs] def global_conf(self):
"""
Returns a dictionary like object, that contains the *global*
configuration of the plugin (:file:`plugins.conf`).
:seealso: :meth:`world_conf`
"""
# Make sure the configuration section exists.
main_conf = self.__app.conf().main()
if not self.__name in main_conf:
log.info(
"creating configuration section for '%s' in '%s'.",
self.__name, main_conf.path()
)
main_conf.add_section(self.__name)
log.info(
"created configuration section for '%s' in '%s'.",
self.__name, main_conf.path()
)
return main_conf[self.__name]
[docs] def world_conf(self, world):
"""
Returns a dictionary like object, that contains the *world* specific
configuration of the plugin (:file:`foo.world.conf`).
:seealso: :meth:`global_conf`
:arg world:
The :class:`WorldWrapper` of the world or the world's name (str).
"""
# Get the world's name.
world_name = world.name() if isinstance(world, WorldWrapper) else world
# Make sure, the configuration section exists.
conf = self.__app.conf().world(world_name)
section_name = "plugin:{}".format(self.__name)
if not section_name in conf:
log.info(
"creating configuration section for '%s' in '%s'.",
self.__name, conf.path()
)
conf.add_section(section_name)
log.info(
"created configuration section for '%s' in '%s'.",
self.__name, conf.path()
)
return conf[section_name]
[docs] def data_dir(self, create=True):
"""
Returns the directory that contains all data created by the plugin
to manage its EMSM job.
:param bool create:
If the directory does not exist, it will be created.
.. seealso::
* :meth:`emsm.core.paths.Pathsystem.plugin_data_dir`
"""
data_dir = self.__app.paths().plugin_data(self.__name)
# Make sure the directory exists.
if not os.path.exists(data_dir) and create:
log.info("creating data directory for '{}'.".format(self.__name))
os.makedirs(data_dir)
log.info("created data directory for '{}'.".format(self.__name))
return data_dir
[docs] def argparser(self):
"""
Returns the :class:`argparse.ArgumentParser` that is used by this
plugin.
If :attr:`HIDDEN` is ``True``, *None* is returned, since the plugin
has no argument parser.
.. seealso::
* :meth:`emsm.core.argparse_.ArgumentParser.plugin_parser`
"""
return self.__argparser
def _uninstall(self):
"""
This method is called by *uninstall()* and should remove all
data or configuration generated by the plugin.
**Subclass:**
* You may completly override this method.
"""
return None
[docs] def uninstall(self):
"""
Called when the plugin should be uninstalled. This method
is interactive and requires the user to confirm if and which
data should be removed.
The BasePlugin removes:
* the plugin module (the *.py* file in *plugins*)
* the plugin data directory
* the plugin configuration
**Subclass:**
Subclasses should override the :meth:`_uninstall` method.
**Signals:**
* :attr:`plugin_uninstalled`
.. seealso::
* :meth:`data_dir`
* :meth:`conf`
* :meth:`_uninstall`
"""
log.info("uninstalling '{}' ...".format(self.__name))
# Make sure the user really wants to uninstall the plugin.
if not userinput.ask("Do you really want to remove '{}'?"\
.format(self.__name)
):
# I did not want to implement a new exception type for this case.
# I think KeyboardInterrupt is good enough.
log.info("cancelled uninstallation of '{}'.".format(self.__name))
return None
# Remove the python module that contains the plugin.
plugin_module = self.__app.plugins().get_module(self.__name)
if plugin_module:
os.remove(plugin_module.__file__)
log.info("removed '{}' module at '{}'."\
.format(self.__name, plugin_module.__file__)
)
# Remove the plugin data directory.
if userinput.ask("Do you want to remove the data directory?"):
shutil.rmtree(self.data_dir(), True)
log.info("removed '{}' plugin data directory at '{}'."\
.format(self.__name, self.data_dir(create=False))
)
# Remove the configuration.
if userinput.ask("Do you want to remove the configuration?"):
self.__app.conf().main().remove_section(self.__name)
log.info("removed '{}' configuration section in '{}'."\
.format(self.__name, self.__app.conf().main().path())
)
# Remove the configuration section in every *.world.conf file.
for world_conf in self.__app.conf().worlds():
world_conf.remove_section("plugin:" + self.name())
# Remove the subclass stuff.
self._uninstall()
# Emit the signal.
BasePlugin.plugin_uninstalled.send(self)
return None
[docs] def run(self, args):
"""
The *main* method of the plugin. This method is called if the plugin
has been invoked by the command line arguments.
:params argparse.Namespace args:
is an argparse.Namespace instance that contains the values
of the parsed command line arguments.
Subclass:
* You may override this method.
.. seealso::
* :meth:`argparser`
* :meth:`emsm.core.argparse_.ArgumentParser.args`
* :meth:`emsm.core.plugins.PluginManager.run`
"""
return None
[docs] def finish(self):
"""
Called when the EMSM application is about to finish. This method can
be used for background jobs or clean up stuff.
Subclass:
* You may override this method.
.. seealso::
* :meth:`emsm.core.plugins.PluginManager.finish`
"""
return None