Initial
This commit is contained in:
118
CHANGELOG.txt
Normal file
118
CHANGELOG.txt
Normal file
@@ -0,0 +1,118 @@
|
||||
version-1.12.0 [2018-09-02]
|
||||
- code: fix yapsy on python3.6
|
||||
- code: Make the test more robust to "unusual" unpacking of the module (see: https://sourceforge.net/p/yapsy/bugs/32/)
|
||||
- code: Protect against providing a single string to setPluginPlaces (see: https://sourceforge.net/p/yapsy/bugs/38/)
|
||||
- code: Enforce the exact directory list provided at construction time (see: https://sourceforge.net/p/yapsy/bugs/36/)
|
||||
- code: Make multiprocess plugin work on windows too ! (see: https://sourceforge.net/p/yapsy/bugs/33/)
|
||||
- code: add a filter-based getter selecting plugins on plugininfo properties (see: https://sourceforge.net/p/yapsy/feature-requests/16/)
|
||||
- code: Add callback_after argument to the LoadPlugins method in PluginManager (contrib https://sourceforge.net/p/yapsy/feature-requests/9/)
|
||||
- code: Rejecting a candidate should not be a warning (contrib Guillaume Binet: https://github.com/tibonihoo/yapsy/pull/7)
|
||||
- code: fix PluginFileLocator __init__ should assignment of plugin_info_cls (contrib Xuecheng Zhang: https://github.com/tibonihoo/yapsy/pull/8)
|
||||
|
||||
version-1.11.223 [2015-06-25]
|
||||
- doc: minor doc fixes
|
||||
|
||||
version-1.11.123 [2015-05-08]
|
||||
|
||||
- code: Make _extractCorePluginInfo accept Unicode filenames (bug https://sourceforge.net/p/yapsy/bugs/30/)
|
||||
- code: fix default change trigger for ConfigurablePluginManager (see https://sourceforge.net/p/yapsy/support-requests/9/)
|
||||
|
||||
version-1.11.023 [2015-04-05]
|
||||
|
||||
- code: merge python3 and default branch (contrib delijati)
|
||||
- code: fix exception catching to support flask use case (contrib delijati: https://github.com/tibonihoo/yapsy/pull/4)
|
||||
- code: fix error reporting (contrib frmdstryr: https://github.com/tibonihoo/yapsy/pull/5)
|
||||
- code: allow plugins to run in separate processes (contrib pylanglois: https://github.com/tibonihoo/yapsy/pull/6)
|
||||
- code: fix dangerous usage of mutable objects as default arguments
|
||||
- doc: added a few badges
|
||||
- doc: added an example of fetching yapsy's development version with pip
|
||||
|
||||
version-1.10.423 [2014-06-07]
|
||||
|
||||
- code: Speed optimisation for the regexp compiled in __init__.py (see https://sourceforge.net/p/yapsy/patches/4/)
|
||||
- code: fix bug "Plugin detection doesn't follow symlinks" (see https://sourceforge.net/p/yapsy/bugs/19/)
|
||||
- doc: add links to coveralls.io for code coverage
|
||||
|
||||
version-1.10.323 [2014-03-23]
|
||||
|
||||
- code: fix PluginInfo properties (see https://sourceforge.net/p/yapsy/bugs/13/)
|
||||
- code: fix ConfigurablePluginManager.loadplugin ignore callback bug reported at https://sourceforge.net/p/yapsy/bugs/17/
|
||||
- code: small improvement to the parse error handling (related to https://sourceforge.net/p/yapsy/bugs/12/)
|
||||
|
||||
version-1.10.223 [2013-12-06]
|
||||
|
||||
- packaging: version name change to comply with PEP440 and resolve pip install problems.
|
||||
- code: fix compatibility with python2.5
|
||||
|
||||
version-1.10.2 [2013-05-22]
|
||||
|
||||
- code: fix compatibility with python2.5
|
||||
- doc: add links to travis-ci and readthedocs.org
|
||||
- code: fix AutoInstall test failures [contrib. Agustin Henze]
|
||||
- code: replace deprecated methods usage (for Python3)
|
||||
|
||||
version-1.10.1 [2013-01-13]
|
||||
|
||||
- code: switch from exec to imp.load_module for plugin loading which also solves https://sourceforge.net/p/yapsy/bugs/9/
|
||||
- doc: add explanation about plugin class detection caveat https://sourceforge.net/p/yapsy/bugs/8/
|
||||
- code: fix unicode bug on python2 version, see https://sourceforge.net/p/yapsy/bugs/10/
|
||||
|
||||
version-1.10 [2012-12-18]
|
||||
|
||||
- code: [contrib. Mathieu Havel] "plugin locators" allow to change the strategy to describe and locate plugins
|
||||
- code: [contrib. Mathieu Clabaut] multiple categories per plugin (cf https://bitbucket.org/matclab/yapsy-mcl)
|
||||
- code: [contrib. Mark Fickett] improve logging
|
||||
- code: Gather detailed information on plugin load error via a callback
|
||||
- code: Extra info to plug-in (eg add extra section or embed the ConfigParser output to the plugin_info), see also https://github.com/tintinweb/yapsy
|
||||
- code: proper config of the default "plugin locator" can stop plugin detection from scanning a directory recursively
|
||||
- code: Enforce a same tab convention everywhere
|
||||
- doc: update list of project using yapsy
|
||||
- doc: highlight the existence of tutorial and link to these ones:
|
||||
- doc: be more helpful to users with an advice/troubleshooting page
|
||||
- doc: add a CHANGELOG.txt file
|
||||
|
||||
version-1.9.2 [2012-07-15]
|
||||
|
||||
- packaging fixes and strange version bumps to workaround pypi.python.org's version handling
|
||||
|
||||
version-1.9 [2011-12-23]
|
||||
|
||||
- ability to load zipped plugins
|
||||
- a separate development branch has been created where the focus is on the compatibility with python3
|
||||
- no more SVN repository (as advertised last year it wasn't kept in sync with the Mercurial repository, and it is now officially dead)
|
||||
- better logging of errors and debug infos
|
||||
- small doc improvement, especially to show how simple it is to interactwith the plugins once they are loaded
|
||||
|
||||
version-1.8 [2010-09-26]
|
||||
|
||||
- the documentation has been refactored and should now go "straight to the point"
|
||||
- the source control is now performed by Mercurial
|
||||
- Filtering manager to filter out plugins that must not be loaded, contributed by Roger Gammans
|
||||
- a getAllPlugins method has been added to the PluginManager to make it easier to access plugins when only the default category is defined
|
||||
- code has been slightly cleaned up and should now be easy to adapt to Python3 via the 2to3 tool.
|
||||
|
||||
version-1.7 [2008-04-09]
|
||||
|
||||
- WARNING: API BREAK ! the arguments for [de]activatePluginByName and getPluginByName are now the other way round: category,name -> name,category="Default"
|
||||
- new AutoInstall manager for automatically installing plugins by copying them in proper place
|
||||
- small improvements to generic code for plugin loading
|
||||
|
||||
version-1.6 [2007-11-10]
|
||||
|
||||
- fix major bug in ConfigurablePluginManager
|
||||
|
||||
version-1.5 [2007-11-03]
|
||||
|
||||
- separation of plugin loading into locate and load contributed by Rob McMullen
|
||||
- package with "Easy install" framework
|
||||
- new forge (https://sourceforge.net/p/yapsy) and independent repo from mathbench
|
||||
|
||||
version-1.1 [2007-09-21]
|
||||
|
||||
- VersionedPlugin manager contributed by Rob McMullen
|
||||
|
||||
version-1.0 [2007-08-26]
|
||||
|
||||
- basic implementation of a PluginManager
|
||||
- ConfigurablePlugin manager that can store information in a ConfigParser compatible file
|
||||
- singleton versions of these plugin managers.
|
36
LICENSE.txt
Normal file
36
LICENSE.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
Yapsy is provided under the BSD-2 clause license (see text below),
|
||||
with the following two exceptions:
|
||||
|
||||
- the "yapsy" icons in artwork/ is licensed under the Creative Commons
|
||||
Attribution-Share Alike 3.0 by Thibauld Nion (see
|
||||
artwork/LICENSE.txt)
|
||||
|
||||
- the compat.py file is licensed under the ISC License by Kenneth
|
||||
Reitz (see yapsy/compat.py).
|
||||
|
||||
|
||||
--------------------
|
||||
BSD 2-clause license
|
||||
--------------------
|
||||
|
||||
Copyright (c) 2007-2015, Thibauld Nion
|
||||
|
||||
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.
|
||||
|
||||
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.
|
11
MANIFEST.in
Normal file
11
MANIFEST.in
Normal file
@@ -0,0 +1,11 @@
|
||||
include README.txt
|
||||
include LICENSE.txt
|
||||
include CHANGELOG.txt
|
||||
include runtests.py
|
||||
|
||||
recursive-include test *.py *-plugin *.zip
|
||||
recursive-include yapsy *.py
|
||||
|
||||
recursive-include artwork *
|
||||
recursive-include doc *
|
||||
prune doc/_build
|
60
PKG-INFO
Normal file
60
PKG-INFO
Normal file
@@ -0,0 +1,60 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: Yapsy
|
||||
Version: 1.12.2
|
||||
Summary: Yet another plugin system
|
||||
Home-page: http://yapsy.sourceforge.net
|
||||
Author: Thibauld Nion
|
||||
Author-email: thibauld@tibonihoo.net
|
||||
License: BSD
|
||||
Description: Yapsy is a small library implementing the core mechanisms needed to
|
||||
build a plugin system into a wider application.
|
||||
|
||||
The main purpose is to depend only on Python's standard libraries and
|
||||
to implement only the basic functionalities needed to detect, load and
|
||||
keep track of several plugins. It supports both Python 2 and 3.
|
||||
|
||||
To use yapsy, make sure that the "yapsy" directory is in your Python
|
||||
loading path and just import the needed class from yapsy (e.g. "from
|
||||
yapsy.PluginManager import PluginManager").
|
||||
|
||||
To see more examples, you can have a look at the unit tests inside the
|
||||
"test" directory or at the "Showcase and tutorials" section of the
|
||||
documentation (http://yapsy.sourceforge.net/#showcase-and-tutorials).
|
||||
|
||||
Please let me know if you find this useful.
|
||||
|
||||
Site of the project: http://yapsy.sourceforge.net/
|
||||
|
||||
List of Contributors:
|
||||
- Thibauld Nion
|
||||
- Rob McMullen
|
||||
- Roger Gammans
|
||||
- Mathieu Havel
|
||||
- Mathieu Clabaut
|
||||
- Mark Fickett
|
||||
- Agustin Henze
|
||||
- qitta
|
||||
- Roberto Alsina
|
||||
- Josip Delic (delijati)
|
||||
- frmdstryr
|
||||
- Pierre-Yves Langlois
|
||||
- Guillaume Binet (gbin)
|
||||
- Blake Oliver (Oliver2213)
|
||||
- Xuecheng Zhang (csuzhangxc)
|
||||
- Marc Brooks (mbrooks-public)
|
||||
|
||||
|
||||
Contributions are welcome as pull requests, patches or tickets on the
|
||||
forge (https://sourceforge.net/projects/yapsy/) or on github
|
||||
(https://github.com/tibonihoo/yapsy).
|
||||
|
||||
Keywords: plugin manager
|
||||
Platform: All
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
41
README.txt
Normal file
41
README.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
Yapsy is a small library implementing the core mechanisms needed to
|
||||
build a plugin system into a wider application.
|
||||
|
||||
The main purpose is to depend only on Python's standard libraries and
|
||||
to implement only the basic functionalities needed to detect, load and
|
||||
keep track of several plugins. It supports both Python 2 and 3.
|
||||
|
||||
To use yapsy, make sure that the "yapsy" directory is in your Python
|
||||
loading path and just import the needed class from yapsy (e.g. "from
|
||||
yapsy.PluginManager import PluginManager").
|
||||
|
||||
To see more examples, you can have a look at the unit tests inside the
|
||||
"test" directory or at the "Showcase and tutorials" section of the
|
||||
documentation (http://yapsy.sourceforge.net/#showcase-and-tutorials).
|
||||
|
||||
Please let me know if you find this useful.
|
||||
|
||||
Site of the project: http://yapsy.sourceforge.net/
|
||||
|
||||
List of Contributors:
|
||||
- Thibauld Nion
|
||||
- Rob McMullen
|
||||
- Roger Gammans
|
||||
- Mathieu Havel
|
||||
- Mathieu Clabaut
|
||||
- Mark Fickett
|
||||
- Agustin Henze
|
||||
- qitta
|
||||
- Roberto Alsina
|
||||
- Josip Delic (delijati)
|
||||
- frmdstryr
|
||||
- Pierre-Yves Langlois
|
||||
- Guillaume Binet (gbin)
|
||||
- Blake Oliver (Oliver2213)
|
||||
- Xuecheng Zhang (csuzhangxc)
|
||||
- Marc Brooks (mbrooks-public)
|
||||
|
||||
|
||||
Contributions are welcome as pull requests, patches or tickets on the
|
||||
forge (https://sourceforge.net/projects/yapsy/) or on github
|
||||
(https://github.com/tibonihoo/yapsy).
|
60
Yapsy.egg-info/PKG-INFO
Normal file
60
Yapsy.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,60 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: Yapsy
|
||||
Version: 1.12.2
|
||||
Summary: Yet another plugin system
|
||||
Home-page: http://yapsy.sourceforge.net
|
||||
Author: Thibauld Nion
|
||||
Author-email: thibauld@tibonihoo.net
|
||||
License: BSD
|
||||
Description: Yapsy is a small library implementing the core mechanisms needed to
|
||||
build a plugin system into a wider application.
|
||||
|
||||
The main purpose is to depend only on Python's standard libraries and
|
||||
to implement only the basic functionalities needed to detect, load and
|
||||
keep track of several plugins. It supports both Python 2 and 3.
|
||||
|
||||
To use yapsy, make sure that the "yapsy" directory is in your Python
|
||||
loading path and just import the needed class from yapsy (e.g. "from
|
||||
yapsy.PluginManager import PluginManager").
|
||||
|
||||
To see more examples, you can have a look at the unit tests inside the
|
||||
"test" directory or at the "Showcase and tutorials" section of the
|
||||
documentation (http://yapsy.sourceforge.net/#showcase-and-tutorials).
|
||||
|
||||
Please let me know if you find this useful.
|
||||
|
||||
Site of the project: http://yapsy.sourceforge.net/
|
||||
|
||||
List of Contributors:
|
||||
- Thibauld Nion
|
||||
- Rob McMullen
|
||||
- Roger Gammans
|
||||
- Mathieu Havel
|
||||
- Mathieu Clabaut
|
||||
- Mark Fickett
|
||||
- Agustin Henze
|
||||
- qitta
|
||||
- Roberto Alsina
|
||||
- Josip Delic (delijati)
|
||||
- frmdstryr
|
||||
- Pierre-Yves Langlois
|
||||
- Guillaume Binet (gbin)
|
||||
- Blake Oliver (Oliver2213)
|
||||
- Xuecheng Zhang (csuzhangxc)
|
||||
- Marc Brooks (mbrooks-public)
|
||||
|
||||
|
||||
Contributions are welcome as pull requests, patches or tickets on the
|
||||
forge (https://sourceforge.net/projects/yapsy/) or on github
|
||||
(https://github.com/tibonihoo/yapsy).
|
||||
|
||||
Keywords: plugin manager
|
||||
Platform: All
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
95
Yapsy.egg-info/SOURCES.txt
Normal file
95
Yapsy.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,95 @@
|
||||
CHANGELOG.txt
|
||||
LICENSE.txt
|
||||
MANIFEST.in
|
||||
README.txt
|
||||
runtests.py
|
||||
setup.py
|
||||
Yapsy.egg-info/PKG-INFO
|
||||
Yapsy.egg-info/SOURCES.txt
|
||||
Yapsy.egg-info/dependency_links.txt
|
||||
Yapsy.egg-info/top_level.txt
|
||||
artwork/LICENSE.txt
|
||||
artwork/yapsy-big.png
|
||||
artwork/yapsy-favicon.ico
|
||||
artwork/yapsy-favicon.png
|
||||
artwork/yapsy.png
|
||||
artwork/yapsy.svg
|
||||
doc/Advices.rst
|
||||
doc/AutoInstallPluginManager.rst
|
||||
doc/ConfigurablePluginManager.rst
|
||||
doc/Extensions.rst
|
||||
doc/FilteredPluginManager.rst
|
||||
doc/IMultiprocessChildPlugin.rst
|
||||
doc/IPlugin.rst
|
||||
doc/IPluginLocator.rst
|
||||
doc/Makefile
|
||||
doc/MultiprocessPluginManager.rst
|
||||
doc/MultiprocessPluginProxy.rst
|
||||
doc/PluginFileLocator.rst
|
||||
doc/PluginInfo.rst
|
||||
doc/PluginManager.rst
|
||||
doc/PluginManagerDecorator.rst
|
||||
doc/VersionedPluginManager.rst
|
||||
doc/conf.py
|
||||
doc/index.rst
|
||||
doc/make.bat
|
||||
test/__init__.py
|
||||
test/test_All.py
|
||||
test/test_AutoInstallPlugin.py
|
||||
test/test_ConfigPlugin.py
|
||||
test/test_ErrorInPlugin.py
|
||||
test/test_FilterPlugin.py
|
||||
test/test_PluginFileLocator.py
|
||||
test/test_PluginInfo.py
|
||||
test/test_SimpleMultiprocessPlugin.py
|
||||
test/test_SimplePlugin.py
|
||||
test/test_Singleton.py
|
||||
test/test_VersionedPlugin.py
|
||||
test/test_settings.py
|
||||
test/plugins/ConfigPlugin.py
|
||||
test/plugins/ErroneousPlugin.py
|
||||
test/plugins/LegacyMultiprocessPlugin.py
|
||||
test/plugins/SimpleMultiprocessPlugin.py
|
||||
test/plugins/SimplePlugin.py
|
||||
test/plugins/VersionedPlugin10.py
|
||||
test/plugins/VersionedPlugin11.py
|
||||
test/plugins/VersionedPlugin111.py
|
||||
test/plugins/VersionedPlugin12.py
|
||||
test/plugins/VersionedPlugin12a1.py
|
||||
test/plugins/configplugin.yapsy-config-plugin
|
||||
test/plugins/configplugin.yapsy-filter-plugin
|
||||
test/plugins/erroneousplugin.yapsy-error-plugin
|
||||
test/plugins/legacymultiprocessplugin.multiprocess-plugin
|
||||
test/plugins/simplemultiprocessplugin.multiprocess-plugin
|
||||
test/plugins/simpleplugin.yapsy-filter-plugin
|
||||
test/plugins/simpleplugin.yapsy-plugin
|
||||
test/plugins/versioned10.version-plugin
|
||||
test/plugins/versioned11.version-plugin
|
||||
test/plugins/versioned111.version-plugin
|
||||
test/plugins/versioned12.version-plugin
|
||||
test/plugins/versioned12a1.version-plugin
|
||||
test/pluginsasdirs/simpleplugin.yapsy-plugin
|
||||
test/pluginsasdirs/SimplePlugin/__init__.py
|
||||
test/pluginstoinstall/AutoInstallPlugin.py
|
||||
test/pluginstoinstall/autoinstallWRONGzipplugin.zip
|
||||
test/pluginstoinstall/autoinstallZIPplugin.zip
|
||||
test/pluginstoinstall/autoinstalldirplugin.yapsy-autoinstall-plugin
|
||||
test/pluginstoinstall/autoinstallplugin.yapsy-autoinstall-plugin
|
||||
test/pluginstoinstall/autoinstalldirplugin/__init__.py
|
||||
yapsy/AutoInstallPluginManager.py
|
||||
yapsy/ConfigurablePluginManager.py
|
||||
yapsy/FilteredPluginManager.py
|
||||
yapsy/IMultiprocessChildPlugin.py
|
||||
yapsy/IMultiprocessPlugin.py
|
||||
yapsy/IPlugin.py
|
||||
yapsy/IPluginLocator.py
|
||||
yapsy/MultiprocessPluginManager.py
|
||||
yapsy/MultiprocessPluginProxy.py
|
||||
yapsy/PluginFileLocator.py
|
||||
yapsy/PluginInfo.py
|
||||
yapsy/PluginManager.py
|
||||
yapsy/PluginManagerDecorator.py
|
||||
yapsy/VersionedPluginManager.py
|
||||
yapsy/__init__.py
|
||||
yapsy/__init___flymake.py
|
||||
yapsy/compat.py
|
1
Yapsy.egg-info/dependency_links.txt
Normal file
1
Yapsy.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
1
Yapsy.egg-info/top_level.txt
Normal file
1
Yapsy.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
||||
yapsy
|
87
artwork/LICENSE.txt
Normal file
87
artwork/LICENSE.txt
Normal file
@@ -0,0 +1,87 @@
|
||||
The "yapsy" icon is copyright (c) 2007-2013, Thibauld Nion
|
||||
|
||||
It is licensed under the Creative Commons Attribution-Share Alike 3.0
|
||||
License. More info about this license can be found there:
|
||||
http://creativecommons.org/licenses/by-sa/3.0
|
||||
|
||||
|
||||
---
|
||||
LICENSE TEXT:
|
||||
---
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE.
|
||||
|
||||
License
|
||||
|
||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
|
||||
|
||||
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
|
||||
|
||||
1. Definitions
|
||||
|
||||
1. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License.
|
||||
2. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License.
|
||||
3. "Creative Commons Compatible License" means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License.
|
||||
4. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership.
|
||||
5. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
|
||||
6. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.
|
||||
7. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast.
|
||||
8. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work.
|
||||
9. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
|
||||
10. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images.
|
||||
11. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium.
|
||||
|
||||
2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws.
|
||||
|
||||
3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
|
||||
|
||||
1. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;
|
||||
2. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified.";
|
||||
3. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,
|
||||
4. to Distribute and Publicly Perform Adaptations.
|
||||
5.
|
||||
|
||||
For the avoidance of doubt:
|
||||
1. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License;
|
||||
2. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and,
|
||||
3. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License.
|
||||
|
||||
The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved.
|
||||
|
||||
4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
|
||||
|
||||
1. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested.
|
||||
2. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License.
|
||||
3. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
|
||||
4. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise.
|
||||
|
||||
5. Representations, Warranties and Disclaimer
|
||||
|
||||
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
|
||||
|
||||
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. Termination
|
||||
|
||||
1. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
|
||||
2. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
|
||||
|
||||
8. Miscellaneous
|
||||
|
||||
1. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
|
||||
2. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
|
||||
3. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
|
||||
4. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
|
||||
5. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
|
||||
6. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law.
|
||||
|
||||
Creative Commons Notice
|
||||
|
||||
Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor.
|
||||
|
||||
Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License.
|
||||
|
||||
Creative Commons may be contacted at http://creativecommons.org/.
|
||||
|
||||
|
||||
|
BIN
artwork/yapsy-big.png
Normal file
BIN
artwork/yapsy-big.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
BIN
artwork/yapsy-favicon.ico
Normal file
BIN
artwork/yapsy-favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
artwork/yapsy-favicon.png
Normal file
BIN
artwork/yapsy-favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
artwork/yapsy.png
Normal file
BIN
artwork/yapsy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
148
artwork/yapsy.svg
Normal file
148
artwork/yapsy.svg
Normal file
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="32"
|
||||
height="32"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.44.1"
|
||||
version="1.0"
|
||||
sodipodi:docbase="/Users/thibauldnion/src/mathbench/yapsy/artwork"
|
||||
sodipodi:docname="yapsy.svg"
|
||||
inkscape:export-filename="/Users/thibauldnion/src/mathbench/yapsy/artwork/yapsy.png"
|
||||
inkscape:export-xdpi="180"
|
||||
inkscape:export-ydpi="180">
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="linearGradient10861">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0.9143731"
|
||||
offset="0"
|
||||
id="stop10863" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop10865" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient10861"
|
||||
id="radialGradient10867"
|
||||
cx="12.582929"
|
||||
cy="9.8906536"
|
||||
fx="12.582929"
|
||||
fy="9.8906536"
|
||||
r="7.4495411"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.824392,0.635295,-0.682309,0.898182,3.587653,-3.424709)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="13.625"
|
||||
inkscape:cx="12.773447"
|
||||
inkscape:cy="16.839873"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="32px"
|
||||
height="32px"
|
||||
inkscape:window-width="1146"
|
||||
inkscape:window-height="743"
|
||||
inkscape:window-x="49"
|
||||
inkscape:window-y="22" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:#ad7fa8;fill-opacity:1;fill-rule:nonzero;stroke:#5c3566;stroke-width:0.35742572;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 9.0271323,0.44327831 C 7.5644394,2.0421634 6.6815261,4.1730484 6.6815261,6.5120688 C 6.6815261,10.463648 10.410492,12.587164 12.787548,15.000929 C 13.679102,15.90625 13.974581,17.012084 13.978967,18.091172 C 10.990898,18.834033 8.7665093,21.538815 8.7665093,24.755672 C 8.7665093,28.544778 11.828064,31.60633 15.617169,31.606332 C 19.406274,31.606332 22.505058,28.544778 22.50506,24.755672 C 22.50506,21.565214 20.317956,18.859736 17.367065,18.091172 C 17.358709,16.989066 17.625378,15.872234 18.484021,15.000929 C 21.022615,12.424898 24.590044,10.46086 24.590043,6.5120688 C 24.590043,4.1693547 23.682804,2.0429983 22.207205,0.44327831 C 21.987968,1.1942643 21.647068,1.9075996 21.201945,2.5282616 C 22.01356,3.6512186 22.50506,5.0223485 22.50506,6.5120688 C 22.505058,10.301174 19.406274,13.362728 15.617169,13.362728 C 11.828064,13.362728 8.7665093,10.301174 8.7665093,6.5120688 C 8.7665093,5.0256216 9.2620598,3.6568606 10.069624,2.5282616 C 9.6242464,1.9072445 9.246351,1.1947546 9.0271323,0.44327831 z "
|
||||
id="path2768" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:white;stroke-width:0.35742572;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.50152905"
|
||||
d="M 8.5463292,1.709161 C 7.0540114,3.8021674 6.4706292,6.6995253 7.6383341,9.0722829 C 8.6041308,10.826628 10.018681,12.143466 11.562012,13.459008 C 12.80155,14.511417 14.232607,15.662865 14.262388,17.426824 C 14.54152,18.103296 14.028074,18.590272 13.386469,18.686882 C 12.216765,19.187729 11.233977,19.817634 10.445153,20.809097 C 9.8409513,21.7296 9.2306969,22.953916 9.1430383,24.172084 C 8.9287258,26.097721 9.7391671,28.033468 10.976182,29.344972 C 12.585794,30.913019 14.988154,31.697407 17.188918,31.060122 C 18.639236,30.714105 19.924045,29.843756 20.758374,28.776711 C 22.108343,27.035076 22.544571,24.621721 21.744407,22.547544 C 21.21139,21.247437 20.498655,20.171107 19.28448,19.423461 C 18.648833,18.88011 17.759981,18.711643 17.109653,18.2401 C 16.790245,16.697268 17.604159,15.200358 18.777201,14.264177 C 20.914053,12.279404 23.513319,10.520116 24.15757,7.5408663 C 24.547911,5.2891448 23.701779,2.9524296 22.322111,1.1879152 C 22.179544,1.8322695 21.396306,2.4652983 21.939294,3.0755592 C 22.934042,4.6273064 22.992812,6.6555317 22.591737,8.4166176 C 22.11426,10.40324 20.67117,11.931544 18.971245,12.953178 C 16.663454,14.072 13.72749,13.927949 11.562108,12.543628 C 10.270422,11.461167 9.0705249,10.144288 8.6646061,8.441591 C 8.1866517,6.4552055 8.4542623,4.2258597 9.6260526,2.5282616 C 9.2046108,2.3175199 9.1642288,1.2681579 8.8493976,1.2848649 C 8.7483748,1.4262969 8.647352,1.567729 8.5463292,1.709161 z "
|
||||
id="path6375" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="opacity:1;fill:url(#radialGradient10867);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;display:inline"
|
||||
id="path10859"
|
||||
sodipodi:cx="9.5045872"
|
||||
sodipodi:cy="10.385321"
|
||||
sodipodi:rx="7.4495411"
|
||||
sodipodi:ry="7.4495411"
|
||||
d="M 16.954128 10.385321 A 7.4495411 7.4495411 0 1 1 2.0550461,10.385321 A 7.4495411 7.4495411 0 1 1 16.954128 10.385321 z"
|
||||
transform="matrix(0.832512,0,0,0.837438,7.646944,16.14696)" />
|
||||
<g
|
||||
style="fill:#ad7fa8;stroke:#5c3566;stroke-width:0.3357341;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="g2806"
|
||||
transform="matrix(1.191419,0,0,1.191419,-3.361337,-4.247934)">
|
||||
<path
|
||||
transform="translate(11.37615,3.798166)"
|
||||
d="M 3.4495413 18.972477 A 0.77064222 0.77064222 0 1 1 1.9082568,18.972477 A 0.77064222 0.77064222 0 1 1 3.4495413 18.972477 z"
|
||||
sodipodi:ry="0.77064222"
|
||||
sodipodi:rx="0.77064222"
|
||||
sodipodi:cy="18.972477"
|
||||
sodipodi:cx="2.678899"
|
||||
id="path2802"
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:0.3357341;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="translate(15.15596,3.798166)"
|
||||
d="M 3.4495413 18.972477 A 0.77064222 0.77064222 0 1 1 1.9082568,18.972477 A 0.77064222 0.77064222 0 1 1 3.4495413 18.972477 z"
|
||||
sodipodi:ry="0.77064222"
|
||||
sodipodi:rx="0.77064222"
|
||||
sodipodi:cy="18.972477"
|
||||
sodipodi:cx="2.678899"
|
||||
id="path2804"
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:0.3357341;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;display:inline"
|
||||
sodipodi:type="arc" />
|
||||
</g>
|
||||
<path
|
||||
id="path9954"
|
||||
d="M 13.764226,28.110079 C 15.085326,28.697235 16.259638,28.62384 17.507344,28.110079"
|
||||
style="fill:#2e3436;fill-rule:evenodd;stroke:#2e3436;stroke-width:0.9999997px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="backup"
|
||||
style="opacity:1;display:none"
|
||||
sodipodi:insensitive="true">
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2783"
|
||||
sodipodi:cx="9.5045872"
|
||||
sodipodi:cy="10.385321"
|
||||
sodipodi:rx="7.4495411"
|
||||
sodipodi:ry="7.4495411"
|
||||
d="M 16.954128 10.385321 A 7.4495411 7.4495411 0 1 1 2.0550461,10.385321 A 7.4495411 7.4495411 0 1 1 16.954128 10.385321 z"
|
||||
transform="matrix(0.773399,0,0,0.773399,8.942738,0.99553)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.4 KiB |
134
doc/Advices.rst
Normal file
134
doc/Advices.rst
Normal file
@@ -0,0 +1,134 @@
|
||||
===================================
|
||||
General advices and troubleshooting
|
||||
===================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
|
||||
Getting code samples
|
||||
--------------------
|
||||
|
||||
Yapsy is used enough for your favorite search provider to have good
|
||||
chances of finding some examples of yapsy being used in the wild.
|
||||
|
||||
However if you wonder how a specific functionality can be used, you
|
||||
can also look at the corresponding unit test (in the test folder
|
||||
packaged with yapsy's sources).
|
||||
|
||||
|
||||
Use the logging system
|
||||
----------------------
|
||||
|
||||
Yapsy uses Python's standard ``logging`` module to record most
|
||||
important events and especially plugin loading failures.
|
||||
|
||||
When developping an application based on yapsy, you'll benefit from
|
||||
looking at the 'debug' level logs, which can easily be done from your
|
||||
application code with the following snippet::
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
Also, please note that yapsy uses a named logger for all its logs, so
|
||||
that you can selectively activage debug logs for yapsy with the
|
||||
following snippet::
|
||||
|
||||
import logging
|
||||
logging.getLogger('yapsy').setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
Categorization by inheritance caveat
|
||||
------------------------------------
|
||||
|
||||
If your application defines various categories of plugins with the yapsy's built-in mechanism for that, please keep in mind the following facts:
|
||||
|
||||
- a plugin instance is attributed to a given category by looking if
|
||||
it is an instance, *even via a subclass*, of the class associated
|
||||
to this category;
|
||||
- a plugin may be attributed to several categories.
|
||||
|
||||
Considering this, and if you consider using several categories, you
|
||||
should consider the following tips:
|
||||
|
||||
- **don't associate any category to ``IPlugin``** (unless you want
|
||||
all plugins to be attributed to the corresponding category)
|
||||
- **design a specific subclass** of ``IPlugin`` for each category
|
||||
- if you want to regroup plugins of some categories into a common
|
||||
category: do this by attributing a subclass of ``IPlugin`` to the
|
||||
common category and attribute to the other categories specific
|
||||
subclasses to this intermediate mother class so that **the plugin
|
||||
class inheritance hierarchy reflects the hierarchy between
|
||||
categories** (and if you want something more complex that a
|
||||
hierarchy, you can consider using mixins).
|
||||
|
||||
|
||||
Plugin class detection caveat
|
||||
-----------------------------
|
||||
|
||||
There must be **only one plugin defined per module**. This means that
|
||||
you can't have two plugin description files pointing at the same
|
||||
module for instance.
|
||||
|
||||
Because of the "categorization by inheritance" system, you **musn't
|
||||
directly import the subclass** of ``IPlugin`` in the main plugin file,
|
||||
instead import its containing module and make your plugin class
|
||||
inherit from ``ContainingModule.SpecificPluginClass`` as in the
|
||||
following example.
|
||||
|
||||
The following code won't work (the class ``MyBasePluginClass`` will be
|
||||
detected as the plugin's implementation instead of ``MyPlugin``)::
|
||||
|
||||
from myapp.plugintypes import MyBasePluginClass
|
||||
|
||||
class MyPlugin(MyBasePluginClass):
|
||||
pass
|
||||
|
||||
Instead you should do the following::
|
||||
|
||||
import myapp.plugintypes as plugintypes
|
||||
|
||||
class MyPlugin(plugintypes.MyBasePluginClass):
|
||||
pass
|
||||
|
||||
|
||||
Plugin packaging
|
||||
----------------
|
||||
|
||||
When packaging plugins in a distutils installer or as parts of an
|
||||
application (like for instance with `py2exe`), you may want to take
|
||||
care about the following points:
|
||||
|
||||
- when you set specific directories where to look for plugins with a
|
||||
hardcoded path, be very carefully about the way you write these
|
||||
paths because depending on the cases **using ``__file__`` or
|
||||
relative paths may be unreliable**. For instance with py2exe, you
|
||||
may want to follow the tips from the `Where Am I FAQ`_.
|
||||
|
||||
- you'd should either **package the plugins as plain Python modules or
|
||||
data files** (if you want to consider you application as the only
|
||||
module), either using the dedicated `setup` argument for `py2exe` or
|
||||
using distutils' `MANIFEST.in`
|
||||
|
||||
- if you do package the plugins as data files, **make sure that their
|
||||
dependencies are correctly indicated as dependencies of your
|
||||
package** (or packaged with you application if you use `py2exe`).
|
||||
|
||||
See also a more detailed example for py2exe on `Simon on Tech's Using python plugin scripts with py2exe`_.
|
||||
|
||||
.. _`Where Am I FAQ`: http://www.py2exe.org/index.cgi/WhereAmI
|
||||
.. _`Simon on Tech's Using python plugin scripts with py2exe`: http://notinthestars.blogspot.com.es/2011/04/using-python-plugin-scripts-with-py2exe.html
|
||||
|
||||
|
||||
Code conventions
|
||||
----------------
|
||||
|
||||
If you intend to modify yapsy's sources and to contribute patches
|
||||
back, please respect the following conventions:
|
||||
|
||||
- CamelCase (upper camel case) for class names and functions
|
||||
- camelCase (lower camel case) for methods
|
||||
- UPPERCASE for global variables (with a few exceptions)
|
||||
- tabulations are used for indentation (and not spaces !)
|
||||
- unit-test each new functionality
|
||||
|
7
doc/AutoInstallPluginManager.rst
Normal file
7
doc/AutoInstallPluginManager.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
AutoInstallPluginManager
|
||||
========================
|
||||
|
||||
.. automodule:: yapsy.AutoInstallPluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
7
doc/ConfigurablePluginManager.rst
Normal file
7
doc/ConfigurablePluginManager.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
ConfigurablePluginManager
|
||||
=========================
|
||||
|
||||
.. automodule:: yapsy.ConfigurablePluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
45
doc/Extensions.rst
Normal file
45
doc/Extensions.rst
Normal file
@@ -0,0 +1,45 @@
|
||||
===================
|
||||
Built-in Extensions
|
||||
===================
|
||||
|
||||
The followig ready-to-use classes give you this exact extra
|
||||
functionality you need for your plugin manager:
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
VersionedPluginManager
|
||||
ConfigurablePluginManager
|
||||
AutoInstallPluginManager
|
||||
FilteredPluginManager
|
||||
MultiprocessPluginManager
|
||||
|
||||
|
||||
The following item offer customization for the way plugins are
|
||||
described and detected:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
PluginFileLocator
|
||||
|
||||
|
||||
If you want to build your own extensions, have a look at the following
|
||||
interfaces:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
IPluginLocator
|
||||
PluginManagerDecorator
|
||||
|
||||
If you want to isolate your plugins in separate processes with the
|
||||
``MultiprocessPluginManager``, you should look at the following
|
||||
classes too:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
IMultiprocessChildPlugin
|
||||
MultiprocessPluginProxy
|
7
doc/FilteredPluginManager.rst
Normal file
7
doc/FilteredPluginManager.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
FilteredPluginManager
|
||||
=====================
|
||||
|
||||
.. automodule:: yapsy.FilteredPluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
10
doc/IMultiprocessChildPlugin.rst
Normal file
10
doc/IMultiprocessChildPlugin.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
========================
|
||||
IMultiprocessChildPlugin
|
||||
========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
.. automodule:: yapsy.IMultiprocessChildPlugin
|
||||
:members:
|
||||
:undoc-members:
|
7
doc/IPlugin.rst
Normal file
7
doc/IPlugin.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
IPlugin
|
||||
=======
|
||||
|
||||
.. automodule:: yapsy.IPlugin
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
6
doc/IPluginLocator.rst
Normal file
6
doc/IPluginLocator.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
IPluginLocator
|
||||
==============
|
||||
|
||||
.. automodule:: yapsy.IPluginLocator
|
||||
:members:
|
||||
:undoc-members:
|
88
doc/Makefile
Normal file
88
doc/Makefile
Normal file
@@ -0,0 +1,88 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf _build/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in _build/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in _build/dirhtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in _build/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in _build/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator _build/qthelp/Yapsy.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile _build/qthelp/Yapsy.qhc"
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in _build/latex."
|
||||
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
||||
"run these through (pdf)latex."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
|
||||
@echo
|
||||
@echo "The overview file is in _build/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in _build/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in _build/doctest/output.txt."
|
12
doc/MultiprocessPluginManager.rst
Normal file
12
doc/MultiprocessPluginManager.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
=========================
|
||||
MultiprocessPluginManager
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
.. automodule:: yapsy.MultiprocessPluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
10
doc/MultiprocessPluginProxy.rst
Normal file
10
doc/MultiprocessPluginProxy.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
=======================
|
||||
MultiprocessPluginProxy
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
.. automodule:: yapsy.MultiprocessPluginProxy
|
||||
:members:
|
||||
:undoc-members:
|
6
doc/PluginFileLocator.rst
Normal file
6
doc/PluginFileLocator.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
PluginFileLocator
|
||||
=================
|
||||
|
||||
.. automodule:: yapsy.PluginFileLocator
|
||||
:members:
|
||||
:undoc-members:
|
8
doc/PluginInfo.rst
Normal file
8
doc/PluginInfo.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
==========
|
||||
PluginInfo
|
||||
==========
|
||||
|
||||
.. automodule:: yapsy.PluginInfo
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
12
doc/PluginManager.rst
Normal file
12
doc/PluginManager.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
=============
|
||||
PluginManager
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
.. automodule:: yapsy.PluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
12
doc/PluginManagerDecorator.rst
Normal file
12
doc/PluginManagerDecorator.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
======================
|
||||
PluginManagerDecorator
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
.. automodule:: yapsy.PluginManagerDecorator
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
7
doc/VersionedPluginManager.rst
Normal file
7
doc/VersionedPluginManager.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
VersionedPluginManager
|
||||
======================
|
||||
|
||||
.. automodule:: yapsy.VersionedPluginManager
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
209
doc/conf.py
Normal file
209
doc/conf.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Yapsy documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Aug 21 19:38:34 2010.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
SRC_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
|
||||
sys.path = [SRC_DIR] + sys.path
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.append(os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Yapsy'
|
||||
copyright = '2007-2018, Thibauld Nion'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
|
||||
import sys
|
||||
sys.path.insert(0,os.path.dirname(__file__))
|
||||
import yapsy
|
||||
# The short X.Y version.
|
||||
version = yapsy.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = yapsy.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of documents that shouldn't be included in the build.
|
||||
#unused_docs = []
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
"sidebarbgcolor" : "#777",
|
||||
"sidebarlinkcolor": "#e0cede",
|
||||
"relbarbgcolor" : "#999",
|
||||
"relbarlinkcolor": "#e0cede",
|
||||
"footerbgcolor" : "#777",
|
||||
"headtextcolor" : "#5c3566",
|
||||
"linkcolor": "#5c3566",
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = os.path.join(SRC_DIR,"artwork","yapsy-big.png")
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = os.path.join(SRC_DIR,"artwork","yapsy-favicon.ico")
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_use_modindex = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Yapsydoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Yapsy.tex', 'Yapsy Documentation',
|
||||
'Thibauld Nion', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_use_modindex = True
|
316
doc/index.rst
Normal file
316
doc/index.rst
Normal file
@@ -0,0 +1,316 @@
|
||||
.. Yapsy documentation master file, created by
|
||||
sphinx-quickstart on Sat Aug 21 19:38:34 2010.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
================================
|
||||
Yapsy: Yet Another Plugin SYstem
|
||||
================================
|
||||
|
||||
*A simple plugin system for Python applications*
|
||||
|
||||
|
||||
.. |Yapsy| replace:: **Yapsy**
|
||||
.. |CC-BYSA| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png
|
||||
:alt: Creative Commons License
|
||||
|
||||
|
||||
Quick links:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
IPlugin
|
||||
PluginManager
|
||||
PluginInfo
|
||||
Extensions
|
||||
Advices
|
||||
|
||||
|
||||
.. contents:: On this page
|
||||
:local:
|
||||
|
||||
|
||||
.. automodule:: yapsy
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. _extend:
|
||||
|
||||
Make it your own
|
||||
================
|
||||
|
||||
For applications that require the plugins and their managers to be
|
||||
more sophisticated, several techniques make such enhancement easy. The
|
||||
following sections detail the most frequent needs for extensions
|
||||
and what you can do about it.
|
||||
|
||||
|
||||
More sophisticated plugin classes
|
||||
---------------------------------
|
||||
|
||||
You can define a plugin class with a richer interface than
|
||||
``IPlugin``, so long as it inherits from IPlugin, it should work the
|
||||
same. The only thing you need to know is that the plugin instance is
|
||||
accessible via the ``PluginInfo`` instance from its
|
||||
``PluginInfo.plugin_object``.
|
||||
|
||||
|
||||
It is also possible to define a wider variety of plugins, by defining
|
||||
as much subclasses of IPlugin. But in such a case you have to inform
|
||||
the manager about that before collecting plugins::
|
||||
|
||||
# Build the manager
|
||||
simplePluginManager = PluginManager()
|
||||
# Tell it the default place(s) where to find plugins
|
||||
simplePluginManager.setPluginPlaces(["path/to/myplugins"])
|
||||
# Define the various categories corresponding to the different
|
||||
# kinds of plugins you have defined
|
||||
simplePluginManager.setCategoriesFilter({
|
||||
"Playback" : IPlaybackPlugin,
|
||||
"SongInfo" : ISongInfoPlugin,
|
||||
"Visualization" : IVisualisation,
|
||||
})
|
||||
|
||||
|
||||
.. note:: Communicating with the plugins belonging to a given category
|
||||
might then be achieved with some code looking like the
|
||||
following::
|
||||
|
||||
# Trigger 'some action' from the "Visualization" plugins
|
||||
for pluginInfo in simplePluginManager.getPluginsOfCategory("Visualization"):
|
||||
pluginInfo.plugin_object.doSomething(...)
|
||||
|
||||
|
||||
Enhance the plugin manager's interface
|
||||
--------------------------------------
|
||||
|
||||
To make the plugin manager more helpful to the other components of an
|
||||
application, you should consider decorating it.
|
||||
|
||||
Actually a "template" for such decoration is provided as
|
||||
:doc:`PluginManagerDecorator`, which must be inherited in order to
|
||||
implement the right decorator for your application.
|
||||
|
||||
Such decorators can be chained, so that you can take advantage of the ready-made decorators such as:
|
||||
|
||||
:doc:`ConfigurablePluginManager`
|
||||
|
||||
Implements a ``PluginManager`` that uses a configuration file to
|
||||
save the plugins to be activated by default and also grants access
|
||||
to this file to the plugins.
|
||||
|
||||
|
||||
:doc:`AutoInstallPluginManager`
|
||||
|
||||
Automatically copy the plugin files to the right plugin directory.
|
||||
|
||||
A full list of pre-implemented decorators is available at :doc:`Extensions`.
|
||||
|
||||
|
||||
Modify plugin descriptions and detections
|
||||
-----------------------------------------
|
||||
|
||||
By default, plugins are described by a text file called the plugin
|
||||
"info file" expected to have a ".yapsy-plugin" extension.
|
||||
|
||||
You may want to use another way to describe and detect your
|
||||
application's plugin and happily yapsy (since version 1.10) makes it
|
||||
possible to provide the ``PluginManager`` with a custom strategy for
|
||||
plugin detection.
|
||||
|
||||
See :doc:`IPluginLocator` for the required interface of such
|
||||
strategies and :doc:`PluginFileLocator` for a working example of such
|
||||
a detection strategy.
|
||||
|
||||
|
||||
Modify the way plugins are loaded
|
||||
---------------------------------
|
||||
|
||||
To tweak the plugin loading phase it is highly advised to re-implement
|
||||
your own manager class.
|
||||
|
||||
The nice thing is, if your new manager inherits ``PluginManager``, then it will naturally fit as the start point of any decoration chain. You just have to provide an instance of this new manager to the first decorators, like in the following::
|
||||
|
||||
# build and configure a specific manager
|
||||
baseManager = MyNewManager()
|
||||
# start decorating this manager to add some more responsibilities
|
||||
myFirstDecorator = AFirstPluginManagerDecorator(baseManager)
|
||||
# add even more stuff
|
||||
mySecondDecorator = ASecondPluginManagerDecorator(myFirstDecorator)
|
||||
|
||||
.. note:: Some decorators have been implemented that modify the way
|
||||
plugins are loaded, this is however not the easiest way to
|
||||
do it and it makes it harder to build a chain of decoration
|
||||
that would include these decorators. Among those are
|
||||
:doc:`VersionedPluginManager` and
|
||||
:doc:`FilteredPluginManager`
|
||||
|
||||
|
||||
Showcase and tutorials
|
||||
======================
|
||||
|
||||
|yapsy| 's development has been originally motivated by the MathBench_
|
||||
project but it is now used in other (more advanced) projects like:
|
||||
|
||||
- peppy_ : "an XEmacs-like editor in Python. Eventually. "
|
||||
- MysteryMachine_ : "an application for writing freeform games."
|
||||
- Aranduka_ : "A simple e-book manager and reader"
|
||||
- err_ : "a plugin based chatbot"
|
||||
- nikola_ : "a Static Site and Blog Generator"
|
||||
|
||||
.. _MathBench: http://mathbench.sourceforge.net
|
||||
.. _peppy: http://www.flipturn.org/peppy/
|
||||
.. _MysteryMachine: http://trac.backslashat.org/MysteryMachine
|
||||
.. _Aranduka: https://github.com/ralsina/aranduka
|
||||
.. _err: http://gbin.github.com/err/
|
||||
.. _nikola: http://nikola.ralsina.com.ar/
|
||||
|
||||
Nowadays, the development is clearly motivated by such external projects and the enthusiast developpers who use the library.
|
||||
|
||||
If you're interested in using yapsy, feel free to look into the following links:
|
||||
|
||||
- :doc:`Advices`
|
||||
- `A minimal example on stackoverflow`_
|
||||
- `Making your app modular: Yapsy`_ (applied to Qt apps)
|
||||
- `Python plugins with yapsy`_ (applied to GTK apps)
|
||||
|
||||
.. _`Making your app modular: Yapsy`: http://ralsina.me/weblog/posts/BB923.html
|
||||
.. _`A minimal example on stackoverflow`: http://stackoverflow.com/questions/5333128/yapsy-minimal-example
|
||||
.. _`Python plugins with yapsy`: https://github.com/MicahCarrick/yapsy-gtk-example
|
||||
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
|
||||
Contributing or forking ?
|
||||
-------------------------
|
||||
|
||||
You're always welcome if you suggest any kind of enhancements, any new
|
||||
decorators or any new pluginmanager. Even more if there is some code
|
||||
coming with it though this is absolutely not compulsory.
|
||||
|
||||
It is also really fine to *fork* the code ! In the past, some people
|
||||
found |yapsy| just good enough to be used as a "code base" for their
|
||||
own plugin system, which they evolved in a more or less incompatible
|
||||
way with the "original" |yapsy|, if you think about it, with such a
|
||||
small library this is actually a clever thing to do.
|
||||
|
||||
In any case, please remember that just providing some feedback on where
|
||||
you're using |yapsy| (original or forked) and how it is useful to you,
|
||||
is in itself a appreciable contribution :)
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The work is placed under the simplified BSD_ license in order to make
|
||||
it as easy as possible to be reused in other projects.
|
||||
|
||||
.. _BSD: http://www.opensource.org/licenses/bsd-license.php
|
||||
|
||||
Please note that the icon is not under the same license but under the
|
||||
`Creative Common Attribution-ShareAlike`_ license.
|
||||
|
||||
.. _`Creative Common Attribution-ShareAlike`: http://creativecommons.org/licenses/by-sa/3.0/
|
||||
|
||||
|
||||
Forge
|
||||
-----
|
||||
|
||||
The project is hosted by `Sourceforge`_ where you can access the code, documentation and a tracker to share your feedback and ask for support.
|
||||
|
||||
|SourceForge.net|
|
||||
|
||||
.. _`Sourceforge`: http://sourceforge.net/projects/yapsy/
|
||||
.. |SourceForge.net| image:: http://sflogo.sourceforge.net/sflogo.php?group_id=208383&type=5
|
||||
:alt: SourceForge.net
|
||||
|
||||
|
||||
**Any suggestion and help are much welcome !**
|
||||
|
||||
Yapsy is also tested on the continous integration service `TravisCI`_:
|
||||
|CITests| |Coverage|
|
||||
|
||||
.. _`TravisCI`: https://travis-ci.org/tibonihoo/yapsy
|
||||
.. |CITests| image:: https://travis-ci.org/tibonihoo/yapsy.png?branch=master
|
||||
:alt: Continuous integration tests
|
||||
.. |Coverage| image:: https://coveralls.io/repos/tibonihoo/yapsy/badge.png?branch=master
|
||||
:alt: Code coverage from continuous integration tests.
|
||||
:target: https://coveralls.io/r/tibonihoo/yapsy?branch=master
|
||||
|
||||
A few alternative sites are available:
|
||||
|
||||
* Yapsy's sources are mirrored on `GitHub`_.
|
||||
|
||||
* To use `pip for a development install`_ you can do something like::
|
||||
|
||||
pip install -e "git+https://github.com/tibonihoo/yapsy.git#egg=yapsy&subdirectory=package"
|
||||
pip install -e "hg+http://hg.code.sf.net/p/yapsy/code#egg=yapsy&subdirectory=package"
|
||||
|
||||
* A development version of the documentation is available on `ReadTheDoc`_.
|
||||
|
||||
|
||||
.. _`GitHub`: https://github.com/tibonihoo/yapsy/
|
||||
.. _`pip for a development install`: http://pip.readthedocs.org/en/latest/reference/pip_install.html#vcs-support
|
||||
.. _`ReadTheDoc`: https://yapsy.readthedocs.org
|
||||
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
Other Python plugin systems already existed before |yapsy| and some
|
||||
have appeared after that. |yapsy|'s creation is by no mean a sign that
|
||||
these others plugin systems sucks :) It is just the results of me
|
||||
being slighlty lazy and as I had already a good idea of how a simple
|
||||
plugin system should look like, I wanted to implement my own
|
||||
[#older_systems]_.
|
||||
|
||||
|
||||
- setuptools_ seems to be designed to allow applications to have a
|
||||
plugin system.
|
||||
|
||||
.. _setuptools: http://cheeseshop.python.org/pypi/setuptools
|
||||
|
||||
|
||||
- Sprinkles_ seems to be also quite lightweight and simple but just
|
||||
maybe too far away from the design I had in mind.
|
||||
|
||||
.. _Sprinkles: http://termie.pbwiki.com/SprinklesPy
|
||||
|
||||
|
||||
- PlugBoard_ is certainly quite good also but too complex for me. It also
|
||||
depends on zope which considered what I want to do here is way too
|
||||
much.
|
||||
|
||||
.. _PlugBoard: https://pypi.python.org/pypi/PlugBoard
|
||||
|
||||
- `Marty Alchin's simple plugin framework`_ is a quite interesting
|
||||
description of a plugin architecture with code snippets as
|
||||
illustrations.
|
||||
|
||||
.. _`Marty Alchin's simple plugin framework`: http://martyalchin.com/2008/jan/10/simple-plugin-framework/
|
||||
|
||||
- stevedor_ looks quite promising and actually seems to make
|
||||
setuptools relevant to build plugin systems.
|
||||
|
||||
.. _stevedor: https://pypi.python.org/pypi/stevedore
|
||||
|
||||
- You can look up more example on a `stackoverflow's discution about minimal plugin systems in Python`_
|
||||
|
||||
.. _`stackoverflow's discution about minimal plugin systems in Python`: http://stackoverflow.com/questions/932069/building-a-minimal-plugin-architecture-in-python
|
||||
|
||||
|
||||
.. [#older_systems] All the more because it seems that my modest
|
||||
design ideas slightly differ from what has been done in other
|
||||
libraries.
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
112
doc/make.bat
Normal file
112
doc/make.bat
Normal file
@@ -0,0 +1,112 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
set SPHINXBUILD=sphinx-build
|
||||
set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (_build\*) do rmdir /q /s %%i
|
||||
del /q /s _build\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in _build/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% _build/dirhtml
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in _build/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% _build/htmlhelp
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in _build/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% _build/qthelp
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in _build/qthelp, like this:
|
||||
echo.^> qcollectiongenerator _build\qthelp\Yapsy.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile _build\qthelp\Yapsy.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in _build/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes
|
||||
echo.
|
||||
echo.The overview file is in _build/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% _build/linkcheck
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in _build/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% _build/doctest
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in _build/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
64
runtests.py
Normal file
64
runtests.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Main file to launch the tests.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import getopt
|
||||
import unittest
|
||||
import logging
|
||||
|
||||
|
||||
from test.test_All import MainTestSuite
|
||||
|
||||
def usage():
|
||||
"""
|
||||
Show/explain the options.
|
||||
"""
|
||||
return """python main.py [OPTIONS]
|
||||
|
||||
Options:
|
||||
|
||||
-h or --help Print this help text
|
||||
|
||||
-d Switch the logger to DEBUG mode.
|
||||
|
||||
-v Switch the test to verbose mode.
|
||||
"""
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""
|
||||
Launch all the test.
|
||||
"""
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "vdh", ["help"])
|
||||
except getopt.GetoptError:
|
||||
print(usage())
|
||||
sys.exit(2)
|
||||
loglevel = logging.ERROR
|
||||
test_verbosity = 1
|
||||
for o,a in opts:
|
||||
if o in ("-h","--help"):
|
||||
print(usage())
|
||||
sys.exit(0)
|
||||
elif o == "-d":
|
||||
loglevel = logging.DEBUG
|
||||
elif o == "-v":
|
||||
test_verbosity = 2
|
||||
logging.basicConfig(level= loglevel,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
# launch the testing process
|
||||
unittest.TextTestRunner(verbosity=test_verbosity).run(MainTestSuite)
|
||||
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main(sys.argv)
|
||||
|
||||
|
||||
|
69
setup.py
Normal file
69
setup.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-
|
||||
|
||||
"""
|
||||
The setup.py script needed to build a .egg for an easier distribution
|
||||
and installation of yapsy.
|
||||
|
||||
Requires 'Easy Install' to be installed :)
|
||||
see there: http://peak.telecommunity.com/DevCenter/EasyInstall#installation-instructions
|
||||
|
||||
Then to create a package run:
|
||||
$ python setup.py bdist_egg
|
||||
|
||||
To use the generated .egg file then:
|
||||
easy_install Yapsy-{yapsy version}-py{python version}.egg
|
||||
|
||||
Automagical stuff:
|
||||
|
||||
- test everything::
|
||||
|
||||
python setup.py test
|
||||
|
||||
- build the packages (sources an egg) and upload all the stuff to pypi::
|
||||
|
||||
python setup.py sdist bdist_egg upload
|
||||
|
||||
- build the documentation
|
||||
|
||||
python setup.py build_sphinx
|
||||
"""
|
||||
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
# just in case setup.py is launched from elsewhere that the containing directory
|
||||
originalDir = os.getcwd()
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
setup(
|
||||
name = "Yapsy",
|
||||
version = __import__("yapsy").__version__,
|
||||
packages = ['yapsy'],
|
||||
package_dir = {'yapsy':'yapsy'},
|
||||
|
||||
# the unit tests
|
||||
test_suite = "test.test_All.MainTestSuite",
|
||||
|
||||
# metadata for upload to PyPI
|
||||
author = "Thibauld Nion",
|
||||
author_email = "thibauld@tibonihoo.net",
|
||||
description = "Yet another plugin system",
|
||||
license = "BSD",
|
||||
keywords = "plugin manager",
|
||||
url = "http://yapsy.sourceforge.net",
|
||||
# more details
|
||||
long_description = open("README.txt").read(),
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules'],
|
||||
platforms='All',
|
||||
)
|
||||
|
||||
finally:
|
||||
os.chdir(originalDir)
|
4
test/__init__.py
Normal file
4
test/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
"""
|
||||
Gathers the unittests of yapsy
|
||||
"""
|
56
test/plugins/ConfigPlugin.py
Normal file
56
test/plugins/ConfigPlugin.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class ConfigPlugin(IPlugin):
|
||||
"""
|
||||
Try to use the methods with which it has been decorated.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
Call the parent class's acivation method
|
||||
"""
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
Just call the parent class's method
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
def choseTestOption(self, value):
|
||||
"""
|
||||
Set an option to a given value.
|
||||
"""
|
||||
self.setConfigOption("Test",value)
|
||||
|
||||
def checkTestOption(self):
|
||||
"""
|
||||
Test if the test option is here.
|
||||
"""
|
||||
return self.hasConfigOption("Test")
|
||||
|
||||
def getTestOption(self):
|
||||
"""
|
||||
Return the value of the test option.
|
||||
"""
|
||||
return self.getConfigOption("Test")
|
||||
|
||||
|
42
test/plugins/ErroneousPlugin.py
Normal file
42
test/plugins/ErroneousPlugin.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
from import_error import the_error_is_here
|
||||
|
||||
class ErrorenousPlugin(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
19
test/plugins/LegacyMultiprocessPlugin.py
Normal file
19
test/plugins/LegacyMultiprocessPlugin.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
A simple multiprocessed plugin that echoes the content received to the parent
|
||||
"""
|
||||
|
||||
from yapsy.IMultiprocessChildPlugin import IMultiprocessChildPlugin
|
||||
|
||||
class LegacyMultiprocessPlugin(IMultiprocessChildPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_pipe):
|
||||
IMultiprocessChildPlugin.__init__(self, parent_pipe=parent_pipe)
|
||||
|
||||
def run(self):
|
||||
content_from_parent = self.parent_pipe.recv()
|
||||
self.parent_pipe.send("{0}|echo_from_child".format(content_from_parent))
|
20
test/plugins/SimpleMultiprocessPlugin.py
Normal file
20
test/plugins/SimpleMultiprocessPlugin.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
A simple multiprocessed plugin that echoes the content received to the parent
|
||||
"""
|
||||
|
||||
from yapsy.IMultiprocessPlugin import IMultiprocessPlugin
|
||||
|
||||
class SimpleMultiprocessPlugin(IMultiprocessPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_pipe):
|
||||
IMultiprocessPlugin.__init__(self, parent_pipe=parent_pipe)
|
||||
|
||||
def run(self):
|
||||
content_from_parent = self.parent_pipe.recv()
|
||||
self.parent_pipe.send("{0}|echo_from_child".format(content_from_parent))
|
||||
|
40
test/plugins/SimplePlugin.py
Normal file
40
test/plugins/SimplePlugin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class SimplePlugin(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
42
test/plugins/VersionedPlugin10.py
Normal file
42
test/plugins/VersionedPlugin10.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from test_settings import TEST_MESSAGE
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class VersionedPlugin10(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
TEST_MESSAGE("Version 1.0")
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
TEST_MESSAGE("Activated Version 1.0!")
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
TEST_MESSAGE("Deactivated Version 1.0!")
|
||||
|
||||
|
41
test/plugins/VersionedPlugin11.py
Normal file
41
test/plugins/VersionedPlugin11.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from test_settings import TEST_MESSAGE
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class VersionedPlugin11(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
TEST_MESSAGE("Version 1.1")
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
41
test/plugins/VersionedPlugin111.py
Normal file
41
test/plugins/VersionedPlugin111.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from test_settings import TEST_MESSAGE
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class VersionedPlugin111(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
TEST_MESSAGE("Version 1.1.1")
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
40
test/plugins/VersionedPlugin12.py
Normal file
40
test/plugins/VersionedPlugin12.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from test_settings import TEST_MESSAGE
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class VersionedPlugin12(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
TEST_MESSAGE("Version 1.2")
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
TEST_MESSAGE("Activated Version 1.2!")
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
TEST_MESSAGE("Deactivated Version 1.2!")
|
40
test/plugins/VersionedPlugin12a1.py
Normal file
40
test/plugins/VersionedPlugin12a1.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from test_settings import TEST_MESSAGE
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class VersionedPlugin12a1(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
TEST_MESSAGE("Version 1.2a1")
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
TEST_MESSAGE("Activated Version 1.2a1!")
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
TEST_MESSAGE("Deactivated Version 1.2a1!")
|
9
test/plugins/configplugin.yapsy-config-plugin
Normal file
9
test/plugins/configplugin.yapsy-config-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Config Plugin
|
||||
Module = ConfigPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin with configuration handling
|
9
test/plugins/configplugin.yapsy-filter-plugin
Normal file
9
test/plugins/configplugin.yapsy-filter-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Config Plugin
|
||||
Module = ConfigPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin with configuration handling
|
9
test/plugins/erroneousplugin.yapsy-error-plugin
Normal file
9
test/plugins/erroneousplugin.yapsy-error-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Erroreous Plugin
|
||||
Module = ErroneousPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://yapsy.sourceforge.net
|
||||
Description = A simple plugin trigger a syntax error for basic testing
|
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Legacy Multiprocess Plugin
|
||||
Module = LegacyMultiprocessPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Pierre-Yves Langlois
|
||||
Version = 0.1
|
||||
Description = A minimal plugin to test multiprocessing
|
||||
Copyright = 2015
|
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Simple Multiprocess Plugin
|
||||
Module = SimpleMultiprocessPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Pierre-Yves Langlois
|
||||
Version = 0.1
|
||||
Description = A minimal plugin to test multiprocessing
|
||||
Copyright = 2015
|
9
test/plugins/simpleplugin.yapsy-filter-plugin
Normal file
9
test/plugins/simpleplugin.yapsy-filter-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Simple Plugin
|
||||
Module = SimplePlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin usefull for basic testing
|
10
test/plugins/simpleplugin.yapsy-plugin
Normal file
10
test/plugins/simpleplugin.yapsy-plugin
Normal file
@@ -0,0 +1,10 @@
|
||||
[Core]
|
||||
Name = Simple Plugin
|
||||
Module = SimplePlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin usefull for basic testing
|
||||
Copyright = 2014
|
9
test/plugins/versioned10.version-plugin
Normal file
9
test/plugins/versioned10.version-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Versioned Plugin
|
||||
Module = VersionedPlugin10
|
||||
|
||||
[Documentation]
|
||||
Author = Rob McMullen
|
||||
Version = 1.0
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin for version testing
|
9
test/plugins/versioned11.version-plugin
Normal file
9
test/plugins/versioned11.version-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Versioned Plugin
|
||||
Module = VersionedPlugin11
|
||||
|
||||
[Documentation]
|
||||
Author = Rob McMullen
|
||||
Version = 1.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin for version testing
|
9
test/plugins/versioned111.version-plugin
Normal file
9
test/plugins/versioned111.version-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Versioned Plugin
|
||||
Module = VersionedPlugin111
|
||||
|
||||
[Documentation]
|
||||
Author = Rob McMullen
|
||||
Version = 1.1.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin for version testing
|
9
test/plugins/versioned12.version-plugin
Normal file
9
test/plugins/versioned12.version-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Versioned Plugin
|
||||
Module = VersionedPlugin12
|
||||
|
||||
[Documentation]
|
||||
Author = Rob McMullen
|
||||
Version = 1.2
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin for version testing
|
9
test/plugins/versioned12a1.version-plugin
Normal file
9
test/plugins/versioned12a1.version-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Versioned Plugin
|
||||
Module = VersionedPlugin12a1
|
||||
|
||||
[Documentation]
|
||||
Author = Rob McMullen
|
||||
Version = 1.2a1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin for version testing
|
39
test/pluginsasdirs/SimplePlugin/__init__.py
Normal file
39
test/pluginsasdirs/SimplePlugin/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class SimplePlugin(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
9
test/pluginsasdirs/simpleplugin.yapsy-plugin
Normal file
9
test/pluginsasdirs/simpleplugin.yapsy-plugin
Normal file
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Simple Plugin
|
||||
Module = SimplePlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin usefull for basic testing
|
40
test/pluginstoinstall/AutoInstallPlugin.py
Normal file
40
test/pluginstoinstall/AutoInstallPlugin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class AutoInstallPlugin(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
BIN
test/pluginstoinstall/autoinstallWRONGzipplugin.zip
Normal file
BIN
test/pluginstoinstall/autoinstallWRONGzipplugin.zip
Normal file
Binary file not shown.
BIN
test/pluginstoinstall/autoinstallZIPplugin.zip
Normal file
BIN
test/pluginstoinstall/autoinstallZIPplugin.zip
Normal file
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Auto Install Dir Plugin
|
||||
Module = autoinstalldirplugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin usefull for basic testing
|
40
test/pluginstoinstall/autoinstalldirplugin/__init__.py
Normal file
40
test/pluginstoinstall/autoinstalldirplugin/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
This is certainly the second simplest plugin ever.
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
class AutoInstallDirPlugin(IPlugin):
|
||||
"""
|
||||
Only trigger the expected test results.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# initialise parent class
|
||||
IPlugin.__init__(self)
|
||||
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
On activation tell that this has been successfull.
|
||||
"""
|
||||
# get the automatic procedure from IPlugin
|
||||
IPlugin.activate(self)
|
||||
return
|
||||
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
On deactivation check that the 'activated' flag was on then
|
||||
tell everything's ok to the test procedure.
|
||||
"""
|
||||
IPlugin.deactivate(self)
|
||||
|
||||
|
||||
|
@@ -0,0 +1,9 @@
|
||||
[Core]
|
||||
Name = Auto Install Plugin
|
||||
Module = AutoInstallPlugin
|
||||
|
||||
[Documentation]
|
||||
Author = Thibauld Nion
|
||||
Version = 0.1
|
||||
Website = http://mathbench.sourceforge.net
|
||||
Description = A simple plugin usefull for basic testing
|
43
test/test_All.py
Normal file
43
test/test_All.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
The test suite that binds them all...
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# set correct loading path for test files
|
||||
sys.path.append(
|
||||
os.path.dirname(
|
||||
os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# load the tests
|
||||
from . import test_SimplePlugin
|
||||
from . import test_Singleton
|
||||
from . import test_ConfigPlugin
|
||||
from . import test_VersionedPlugin
|
||||
from . import test_AutoInstallPlugin
|
||||
from . import test_FilterPlugin
|
||||
from . import test_ErrorInPlugin
|
||||
from . import test_PluginFileLocator
|
||||
from . import test_PluginInfo
|
||||
from . import test_SimpleMultiprocessPlugin
|
||||
|
||||
# add them to a common test suite
|
||||
MainTestSuite = unittest.TestSuite(
|
||||
[ # add the tests suites below
|
||||
test_SimplePlugin.suite,
|
||||
test_Singleton.suite,
|
||||
test_ConfigPlugin.suite,
|
||||
test_VersionedPlugin.suite,
|
||||
test_AutoInstallPlugin.suite,
|
||||
test_FilterPlugin.suite,
|
||||
test_ErrorInPlugin.suite,
|
||||
test_PluginFileLocator.suite,
|
||||
test_PluginInfo.suite,
|
||||
test_SimpleMultiprocessPlugin.suite,
|
||||
])
|
||||
|
295
test/test_AutoInstallPlugin.py
Normal file
295
test/test_AutoInstallPlugin.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from yapsy.AutoInstallPluginManager import AutoInstallPluginManager
|
||||
|
||||
class AutoInstallTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct installation and loading of a simple plugin.
|
||||
"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
self.storing_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")
|
||||
self.pluginManager = AutoInstallPluginManager(
|
||||
self.storing_dir,
|
||||
directories_list=[self.storing_dir],
|
||||
plugin_info_ext="yapsy-autoinstall-plugin")
|
||||
# load the plugins that may be found
|
||||
self.pluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
self.new_plugins_waiting_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"pluginstoinstall")
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean the plugin installation directory.
|
||||
"""
|
||||
try:
|
||||
os.remove(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"autoinstallplugin.yapsy-autoinstall-plugin"))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.remove(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"AutoInstallPlugin.py"))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.remove(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"autoinstalldirplugin.yapsy-autoinstall-plugin"))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
shutil.rmtree(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"autoinstalldirplugin"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def plugin_loading_check_none(self):
|
||||
"""
|
||||
Test that no plugin has been loaded.
|
||||
"""
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.pluginManager.getCategories()),1)
|
||||
sole_category = self.pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),0)
|
||||
|
||||
def plugin_loading_check(self,new_plugin_name):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.pluginManager.getCategories()),1)
|
||||
sole_category = self.pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0]
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,new_plugin_name)
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testGetSetInstallDir(self):
|
||||
"""
|
||||
Test getting and setting install dir.
|
||||
"""
|
||||
self.assertEqual(self.storing_dir,self.pluginManager.getInstallDir())
|
||||
custom_install_dir = os.path.join("mouf", "bla")
|
||||
self.pluginManager.setInstallDir(custom_install_dir)
|
||||
self.assertEqual(custom_install_dir, self.pluginManager.getInstallDir())
|
||||
|
||||
|
||||
def testNoneLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check_none()
|
||||
|
||||
def testInstallFile(self):
|
||||
"""
|
||||
Test if the correct plugin (defined by a file) can be installed and loaded.
|
||||
"""
|
||||
install_success = self.pluginManager.install(self.new_plugins_waiting_dir,
|
||||
"autoinstallplugin.yapsy-autoinstall-plugin")
|
||||
self.assertTrue(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check("Auto Install Plugin")
|
||||
|
||||
|
||||
def testInstallDir(self):
|
||||
"""
|
||||
Test if the correct plugin (define by a directory) can be installed and loaded.
|
||||
"""
|
||||
install_success = self.pluginManager.install(self.new_plugins_waiting_dir,
|
||||
"autoinstalldirplugin.yapsy-autoinstall-plugin")
|
||||
self.assertTrue(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check("Auto Install Dir Plugin")
|
||||
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
install_success = self.pluginManager.install(self.new_plugins_waiting_dir,
|
||||
"autoinstallplugin.yapsy-autoinstall-plugin")
|
||||
self.assertTrue(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check("Auto Install Plugin")
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
class AutoInstallZIPTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct installation and loading of a zipped plugin.
|
||||
"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
storing_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")
|
||||
self.pluginManager = AutoInstallPluginManager(
|
||||
storing_dir,
|
||||
directories_list=[storing_dir],
|
||||
plugin_info_ext="yapsy-autoinstall-plugin")
|
||||
# load the plugins that may be found
|
||||
self.pluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
self.new_plugins_waiting_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"pluginstoinstall")
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean the plugin installation directory.
|
||||
"""
|
||||
try:
|
||||
os.remove(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"autoinstallzipplugin.yapsy-autoinstall-plugin"))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
shutil.rmtree(os.path.join(self.pluginManager.plugins_places[0],
|
||||
"autoinstallzipplugin"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def plugin_loading_check_none(self):
|
||||
"""
|
||||
Test that no plugin has been loaded.
|
||||
"""
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.pluginManager.getCategories()),1)
|
||||
sole_category = self.pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),0)
|
||||
|
||||
def plugin_loading_check(self,new_plugin_name):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.pluginManager.getCategories()),1)
|
||||
sole_category = self.pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0]
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,new_plugin_name)
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testNoneLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check_none()
|
||||
|
||||
def testInstallZIP(self):
|
||||
"""
|
||||
Test if the correct plugin (define by a zip file) can be installed and loaded.
|
||||
"""
|
||||
test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallZIPplugin.zip")
|
||||
if sys.version_info < (2, 6):
|
||||
self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file)
|
||||
return
|
||||
install_success = self.pluginManager.installFromZIP(test_file)
|
||||
self.assertTrue(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check("Auto Install ZIP Plugin")
|
||||
|
||||
def testInstallZIPFailOnWrongZip(self):
|
||||
"""
|
||||
Test if, when the zip file does not contain what is required the installation fails.
|
||||
"""
|
||||
test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallWRONGzipplugin.zip")
|
||||
if sys.version_info < (2, 6):
|
||||
self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file)
|
||||
return
|
||||
install_success = self.pluginManager.installFromZIP(test_file)
|
||||
self.assertFalse(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check_none()
|
||||
|
||||
def testInstallZIPFailOnUnexistingFile(self):
|
||||
"""
|
||||
Test if, when the zip file is not a file.
|
||||
"""
|
||||
test_file = os.path.join(self.new_plugins_waiting_dir,"doesNotExists.zip")
|
||||
if sys.version_info < (2, 6):
|
||||
self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file)
|
||||
return
|
||||
install_success = self.pluginManager.installFromZIP(test_file)
|
||||
self.assertFalse(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check_none()
|
||||
|
||||
def testInstallZIPFailOnNotAZipFile(self):
|
||||
"""
|
||||
Test if, when the zip file is not a valid zip.
|
||||
"""
|
||||
test_file = os.path.join(self.new_plugins_waiting_dir,"AutoInstallPlugin.py")
|
||||
if sys.version_info < (2, 6):
|
||||
self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file)
|
||||
return
|
||||
install_success = self.pluginManager.installFromZIP(test_file)
|
||||
self.assertFalse(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check_none()
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallZIPplugin.zip")
|
||||
if sys.version_info < (2, 6):
|
||||
self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file)
|
||||
return
|
||||
install_success = self.pluginManager.installFromZIP(test_file)
|
||||
self.assertTrue(install_success)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check("Auto Install ZIP Plugin")
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(AutoInstallTestsCase),
|
||||
unittest.TestLoader().loadTestsFromTestCase(AutoInstallZIPTestsCase),
|
||||
])
|
179
test/test_ConfigPlugin.py
Normal file
179
test/test_ConfigPlugin.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from yapsy.compat import ConfigParser
|
||||
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
|
||||
|
||||
|
||||
class ConfigTestMixin:
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.pluginManager.getCategories()),1)
|
||||
sole_category = self.pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0]
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,"Config Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def plugin_activate(self):
|
||||
"""
|
||||
Activate the plugin with basic checking
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
class ConfigTestCase(unittest.TestCase, ConfigTestMixin):
|
||||
"""
|
||||
Test the correct loading of a plugin that uses a configuration
|
||||
file through a ConfigurablePluginManager as well as basic
|
||||
commands.
|
||||
"""
|
||||
|
||||
CONFIG_FILE = test_settings.TEMP_CONFIG_FILE_NAME
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create a config file
|
||||
self.config_file = self.CONFIG_FILE
|
||||
self.config_parser = ConfigParser()
|
||||
self.plugin_info = None
|
||||
# create the plugin manager
|
||||
self.pluginManager = ConfigurablePluginManager(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="yapsy-config-plugin",
|
||||
configparser_instance=self.config_parser,
|
||||
config_change_trigger=self.update_config)
|
||||
# load the plugins that may be found
|
||||
self.pluginManager.collectPlugins()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
When the test has been performed erase the temp file.
|
||||
"""
|
||||
if os.path.isfile(self.config_file):
|
||||
os.remove(self.config_file)
|
||||
|
||||
def testConfigurationFileExistence(self):
|
||||
"""
|
||||
Test if the configuration file has been properly written.
|
||||
"""
|
||||
# activate the only loaded plugin
|
||||
self.plugin_activate()
|
||||
# get rid of the plugin manager and create a new one
|
||||
del self.pluginManager
|
||||
del self.config_parser
|
||||
self.config_parser = ConfigParser()
|
||||
self.config_parser.read(self.config_file)
|
||||
self.assertTrue(self.config_parser.has_section("Plugin Management"))
|
||||
self.assertTrue(self.config_parser.has_option("Plugin Management",
|
||||
"default_plugins_to_load"))
|
||||
self.pluginManager = ConfigurablePluginManager(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="yapsy-config-plugin",
|
||||
configparser_instance=self.config_parser,
|
||||
config_change_trigger=self.update_config)
|
||||
self.pluginManager.collectPlugins()
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.pluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
# check that activating the plugin once again, won't cause an error
|
||||
self.pluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation/deactivaion procedures work.
|
||||
"""
|
||||
self.plugin_activate()
|
||||
self.pluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
def testPluginOptions(self):
|
||||
"""
|
||||
Test is the plugin can register and access options from the
|
||||
ConfigParser.
|
||||
"""
|
||||
self.plugin_activate()
|
||||
plugin = self.plugin_info.plugin_object
|
||||
plugin.choseTestOption("voila")
|
||||
self.assertTrue(plugin.checkTestOption())
|
||||
self.assertEqual(plugin.getTestOption(),"voila")
|
||||
|
||||
def update_config(self):
|
||||
"""
|
||||
Write the content of the ConfigParser in a file.
|
||||
"""
|
||||
cf = open(self.config_file,"a")
|
||||
self.config_parser.write(cf)
|
||||
cf.close()
|
||||
|
||||
|
||||
class ConfigurablePMWithDefaultChangeTriggerTestCase(unittest.TestCase, ConfigTestMixin):
|
||||
"""Test the correctness of default values of args specific to the
|
||||
ConfigurablePM in its construtor.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create a config file
|
||||
self.config_parser = ConfigParser()
|
||||
self.plugin_info = None
|
||||
# create the plugin manager
|
||||
self.pluginManager = ConfigurablePluginManager(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="yapsy-config-plugin",
|
||||
configparser_instance=self.config_parser)
|
||||
# load the plugins that may be found
|
||||
self.pluginManager.collectPlugins()
|
||||
|
||||
def testPluginOptions(self):
|
||||
"""
|
||||
Test is the plugin can register and access options from the
|
||||
ConfigParser.
|
||||
"""
|
||||
self.plugin_activate()
|
||||
plugin = self.plugin_info.plugin_object
|
||||
plugin.choseTestOption("voila")
|
||||
self.assertTrue(plugin.checkTestOption())
|
||||
self.assertEqual(plugin.getTestOption(),"voila")
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(ConfigTestCase),
|
||||
unittest.TestLoader().loadTestsFromTestCase(ConfigurablePMWithDefaultChangeTriggerTestCase),
|
||||
])
|
68
test/test_ErrorInPlugin.py
Normal file
68
test/test_ErrorInPlugin.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import logging
|
||||
|
||||
from yapsy.PluginManager import PluginManager
|
||||
from yapsy import log
|
||||
|
||||
class ErrorTestCase(unittest.TestCase):
|
||||
"""
|
||||
Test the handling of errors during plugin load.
|
||||
"""
|
||||
|
||||
def testTwoStepsLoadWithError(self):
|
||||
"""
|
||||
Test loading the plugins in two steps in order to collect more
|
||||
deltailed informations and take care of an erroneous plugin.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")
|
||||
], plugin_info_ext="yapsy-error-plugin")
|
||||
# trigger the first step to look up for plugins
|
||||
spm.locatePlugins()
|
||||
# make full use of the "feedback" the loadPlugins can give
|
||||
# - set-up the callback function that will be called *before*
|
||||
# loading each plugin
|
||||
callback_infos = []
|
||||
def preload_cbk(i_plugin_info):
|
||||
callback_infos.append(i_plugin_info)
|
||||
callback_after_infos = []
|
||||
def postload_cbk(i_plugin_info):
|
||||
callback_after_infos.append(i_plugin_info)
|
||||
# - gather infos about the processed plugins (loaded or not)
|
||||
# and for the test, monkey patch the logger
|
||||
originalLogLevel = log.getEffectiveLevel()
|
||||
log.setLevel(logging.ERROR)
|
||||
errorLogCallFlag = [False]
|
||||
def errorMock(*args,**kwargs):
|
||||
errorLogCallFlag[0]=True
|
||||
originalErrorMethod = log.error
|
||||
log.error = errorMock
|
||||
try:
|
||||
loadedPlugins = spm.loadPlugins(callback=preload_cbk, callback_after=postload_cbk)
|
||||
finally:
|
||||
log.setLevel(originalLogLevel)
|
||||
log.error = originalErrorMethod
|
||||
self.assertTrue(errorLogCallFlag[0])
|
||||
self.assertEqual(len(loadedPlugins),1)
|
||||
self.assertEqual(len(callback_infos),1)
|
||||
self.assertTrue(isinstance(callback_infos[0].error,tuple))
|
||||
self.assertEqual(loadedPlugins[0],callback_infos[0])
|
||||
self.assertTrue(issubclass(callback_infos[0].error[0],ImportError))
|
||||
self.assertEqual(len(callback_after_infos),0)
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0)
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(ErrorTestCase),
|
||||
])
|
244
test/test_FilterPlugin.py
Normal file
244
test/test_FilterPlugin.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
from .test_settings import TEST_MESSAGE
|
||||
import unittest
|
||||
import os
|
||||
import re
|
||||
|
||||
from yapsy.FilteredPluginManager import FilteredPluginManager
|
||||
|
||||
|
||||
class testFilter(FilteredPluginManager):
|
||||
"""
|
||||
Test filter class.
|
||||
Refused to load plugins whose Name starts with 'C'.
|
||||
"""
|
||||
_bannednames = re.compile("^C")
|
||||
|
||||
def isPluginOk(self,info):
|
||||
return not self._bannednames.match(info.name)
|
||||
|
||||
|
||||
class FilteredTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading of a simple plugin as well as basic
|
||||
commands.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
# print os.path.join(os.path.dirname(os.path.abspath(__file__)),"plugins")
|
||||
self.filteredPluginManager = testFilter(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="yapsy-filter-plugin",
|
||||
)
|
||||
# load the plugins that may be found
|
||||
self.filteredPluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugins have been loaded.
|
||||
"""
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.filteredPluginManager.getCategories()),1)
|
||||
sole_category = self.filteredPluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.filteredPluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
plugins = self.filteredPluginManager.getPluginsOfCategory(sole_category)
|
||||
for plugin_info in plugins:
|
||||
TEST_MESSAGE("plugin info: %s" % plugin_info)
|
||||
self.plugin_info = plugin_info
|
||||
self.assertTrue(self.plugin_info)
|
||||
self.assertEqual(self.plugin_info.name,"Simple Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object)
|
||||
self.plugin_info.plugin_object.activate()
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.plugin_info.plugin_object.deactivate()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
def testRejectedList(self):
|
||||
"""
|
||||
Test if the list of rejected plugins is correct.
|
||||
"""
|
||||
for plugin in self.filteredPluginManager.getRejectedPlugins():
|
||||
TEST_MESSAGE("plugin info: %s" % plugin[2])
|
||||
self.assertEqual(plugin[2].name,"Config Plugin")
|
||||
|
||||
def testRejectedStable(self):
|
||||
reject1 = list(self.filteredPluginManager.getRejectedPlugins())
|
||||
self.filteredPluginManager.collectPlugins()
|
||||
reject2 = list(self.filteredPluginManager.getRejectedPlugins())
|
||||
self.assertEqual(len(reject1),len(reject2))
|
||||
|
||||
|
||||
def testRejectPlugin(self):
|
||||
self.filteredPluginManager.locatePlugins()
|
||||
rejected = self.filteredPluginManager.rejectedPlugins
|
||||
#If this fails the test in not meaningful..
|
||||
self.assertTrue(len(rejected) > 0)
|
||||
nrRejected = len(rejected)
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.rejectPluginCandidate(plugin)
|
||||
self.assertEqual(nrRejected,len(self.filteredPluginManager.rejectedPlugins))
|
||||
|
||||
def testRemovePlugin(self):
|
||||
self.filteredPluginManager.locatePlugins()
|
||||
rejected = self.filteredPluginManager.rejectedPlugins
|
||||
nrCandidates = len(self.filteredPluginManager.getPluginCandidates())
|
||||
#If this fails the test in not meaningful..
|
||||
self.assertTrue(len(rejected) > 0)
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.removePluginCandidate(plugin)
|
||||
self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins))
|
||||
self.assertEqual( nrCandidates , len(self.filteredPluginManager.getPluginCandidates()))
|
||||
|
||||
def testAppendRejectedPlugin(self):
|
||||
self.filteredPluginManager.locatePlugins()
|
||||
rejected = self.filteredPluginManager.getRejectedPlugins()
|
||||
nrRejected = len(rejected)
|
||||
nrCandidates = len(self.filteredPluginManager.getPluginCandidates())
|
||||
|
||||
#If this fails the test in not meaningful..
|
||||
self.assertTrue(len(rejected) > 0)
|
||||
#Remove the rejected plugins into out own list.
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.removePluginCandidate(plugin)
|
||||
self.assertEqual(len(self.filteredPluginManager.getRejectedPlugins()),0)
|
||||
|
||||
##Now Actually test Append.
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.appendPluginCandidate(plugin)
|
||||
self.assertEqual(nrRejected ,len(self.filteredPluginManager.rejectedPlugins))
|
||||
self.assertEqual(nrCandidates , len(self.filteredPluginManager.getPluginCandidates()))
|
||||
|
||||
def testAppendOkPlugins(self):
|
||||
self.filteredPluginManager.locatePlugins()
|
||||
rejected = self.filteredPluginManager.getRejectedPlugins()
|
||||
nrRejected = len(rejected)
|
||||
nrCandidates = len(self.filteredPluginManager.getPluginCandidates())
|
||||
|
||||
#If this fails the test in not meaningful..
|
||||
self.assertTrue(len(rejected) > 0)
|
||||
#Remove the rejected plugins again.
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.removePluginCandidate(plugin)
|
||||
self.assertEqual(len(self.filteredPluginManager.getRejectedPlugins()),0)
|
||||
|
||||
for plugin in rejected:
|
||||
#change the name so it is acceptable.
|
||||
plugin[2].name = "X" + plugin[2].name[1:]
|
||||
self.filteredPluginManager.appendPluginCandidate(plugin)
|
||||
self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins))
|
||||
self.assertEqual(nrRejected + nrCandidates , len(self.filteredPluginManager.getPluginCandidates()))
|
||||
|
||||
|
||||
|
||||
|
||||
def testUnrejectPlugin(self):
|
||||
self.filteredPluginManager.locatePlugins()
|
||||
rejected = self.filteredPluginManager.rejectedPlugins
|
||||
nrRejected = len(rejected)
|
||||
nrCandidates = len(self.filteredPluginManager.getPluginCandidates())
|
||||
#If this fails the test in not meaningful..
|
||||
self.assertTrue(len(rejected) > 0)
|
||||
for plugin in rejected:
|
||||
self.filteredPluginManager.unrejectPluginCandidate(plugin)
|
||||
self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins))
|
||||
self.assertEqual( nrRejected + nrCandidates ,
|
||||
len(self.filteredPluginManager.getPluginCandidates()))
|
||||
|
||||
|
||||
class FilteredWithMonkeyPathTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading oand filtering of plugins when the FilteredPluginManager is just monkey-patched
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
# print os.path.join(os.path.dirname(os.path.abspath(__file__)),"plugins")
|
||||
self.filteredPluginManager = FilteredPluginManager(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="yapsy-filter-plugin",
|
||||
)
|
||||
self.filteredPluginManager.isPluginOk = lambda info:not re.match("^C",info.name)
|
||||
# load the plugins that may be found
|
||||
self.filteredPluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugins have been loaded.
|
||||
"""
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.filteredPluginManager.getCategories()),1)
|
||||
sole_category = self.filteredPluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.filteredPluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
plugins = self.filteredPluginManager.getPluginsOfCategory(sole_category)
|
||||
for plugin_info in plugins:
|
||||
TEST_MESSAGE("plugin info: %s" % plugin_info)
|
||||
self.plugin_info = plugin_info
|
||||
self.assertTrue(self.plugin_info)
|
||||
self.assertEqual(self.plugin_info.name,"Simple Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object)
|
||||
self.plugin_info.plugin_object.activate()
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.plugin_info.plugin_object.deactivate()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
def testRejectedList(self):
|
||||
"""
|
||||
Test if the list of rejected plugins is correct.
|
||||
"""
|
||||
for plugin in self.filteredPluginManager.getRejectedPlugins():
|
||||
TEST_MESSAGE("plugin info: %s" % plugin[2])
|
||||
self.assertEqual(plugin[2].name,"Config Plugin")
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(FilteredTestsCase),
|
||||
unittest.TestLoader().loadTestsFromTestCase(FilteredWithMonkeyPathTestsCase),
|
||||
])
|
523
test/test_PluginFileLocator.py
Normal file
523
test/test_PluginFileLocator.py
Normal file
@@ -0,0 +1,523 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from yapsy.compat import ConfigParser, StringIO, str, builtin_str
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
import yapsy
|
||||
from yapsy import PLUGIN_NAME_FORBIDEN_STRING
|
||||
from yapsy.PluginManager import PluginManager
|
||||
from yapsy.PluginManager import IPlugin
|
||||
from yapsy.PluginInfo import PluginInfo
|
||||
from yapsy.IPluginLocator import IPluginLocator
|
||||
from yapsy.PluginFileLocator import PluginFileLocator
|
||||
from yapsy.PluginFileLocator import PluginFileAnalyzerWithInfoFile
|
||||
from yapsy.PluginFileLocator import PluginFileAnalyzerMathingRegex
|
||||
|
||||
|
||||
class IPluginLocatorTest(unittest.TestCase):
|
||||
|
||||
|
||||
def test_deprecated_method_dont_raise_notimplemetederror(self):
|
||||
class DummyPluginLocator(IPluginLocator):
|
||||
pass
|
||||
dpl = DummyPluginLocator()
|
||||
self.assertEqual((None,None,None),dpl.getPluginNameAndModuleFromStream(None))
|
||||
dpl.setPluginInfoClass(PluginInfo)
|
||||
self.assertEqual(None,dpl.getPluginInfoClass())
|
||||
dpl.setPluginPlaces([])
|
||||
dpl.updatePluginPlaces([])
|
||||
|
||||
class PluginFileAnalyzerWithInfoFileTest(unittest.TestCase):
|
||||
"""
|
||||
Test that the "info file" analyzer enforces the correct policy.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
self.plugin_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"plugins")
|
||||
self.yapsy_plugin_path = os.path.join(self.plugin_directory,"simpleplugin.yapsy-plugin")
|
||||
self.version_plugin_path = os.path.join(self.plugin_directory,"versioned11.version-plugin")
|
||||
self.yapsy_filter_plugin_path = os.path.join(self.plugin_directory,"simpleplugin.yapsy-filter-plugin")
|
||||
|
||||
def test_Contruction(self):
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf")
|
||||
self.assertEqual(analyzer.name,"mouf")
|
||||
|
||||
def test_isValid(self):
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf")
|
||||
self.assertTrue(analyzer.isValidPlugin(self.yapsy_plugin_path))
|
||||
self.assertFalse(analyzer.isValidPlugin(self.version_plugin_path))
|
||||
|
||||
def test_getInfosDictFromPlugin(self):
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf")
|
||||
info_dict,cf_parser = analyzer.getInfosDictFromPlugin(self.plugin_directory,
|
||||
os.path.basename(self.yapsy_plugin_path))
|
||||
self.assertEqual(info_dict,
|
||||
{'website': 'http://mathbench.sourceforge.net',
|
||||
'description': 'A simple plugin usefull for basic testing',
|
||||
'author': 'Thibauld Nion',
|
||||
'version': '0.1',
|
||||
'path': '%s' % os.path.join(self.plugin_directory,"SimplePlugin"),
|
||||
'name': 'Simple Plugin',
|
||||
'copyright': '2014'})
|
||||
self.assertTrue(isinstance(cf_parser,ConfigParser))
|
||||
|
||||
def test_isValid_WithMultiExtensions(self):
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf",("yapsy-plugin","yapsy-filter-plugin"))
|
||||
self.assertTrue(analyzer.isValidPlugin(self.yapsy_plugin_path))
|
||||
self.assertFalse(analyzer.isValidPlugin(self.version_plugin_path))
|
||||
self.assertTrue(analyzer.isValidPlugin(self.yapsy_filter_plugin_path))
|
||||
|
||||
def test__extractCorePluginInfo_with_builtin_str_filename(self):
|
||||
plugin_desc_content = builtin_str("simpleplugin.yapsy-plugin")
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf", ("yapsy-plugin"))
|
||||
infos, parser = analyzer._extractCorePluginInfo(self.plugin_directory,
|
||||
plugin_desc_content)
|
||||
self.assertEqual("Simple Plugin", infos["name"])
|
||||
self.assertEqual(os.path.join(self.plugin_directory, "SimplePlugin"), infos["path"])
|
||||
|
||||
def test__extractCorePluginInfo_with_unicode_filename(self):
|
||||
"""Note: this test is redundant with its 'builtin_str' counterpart on Python3
|
||||
but not on Python2"""
|
||||
# Note: compat.py redefines str as unicode for Python2
|
||||
plugin_desc_content = str("simpleplugin.yapsy-plugin")
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf", ("yapsy-plugin"))
|
||||
infos, parser = analyzer._extractCorePluginInfo(self.plugin_directory,
|
||||
plugin_desc_content)
|
||||
self.assertEqual("Simple Plugin", infos["name"])
|
||||
self.assertEqual(os.path.join(self.plugin_directory, "SimplePlugin"), infos["path"])
|
||||
|
||||
def test__extractCorePluginInfo_with_minimal_description(self):
|
||||
plugin_desc_content = StringIO("""\
|
||||
[Core]
|
||||
Name = Simple Plugin
|
||||
Module = SimplePlugin
|
||||
""")
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf",
|
||||
("yapsy-plugin"))
|
||||
infos, parser = analyzer._extractCorePluginInfo("bla",plugin_desc_content)
|
||||
self.assertEqual("Simple Plugin", infos["name"])
|
||||
self.assertEqual(os.path.join("bla","SimplePlugin"), infos["path"])
|
||||
self.assertTrue(isinstance(parser,ConfigParser))
|
||||
|
||||
def test_getPluginNameAndModuleFromStream_with_invalid_descriptions(self):
|
||||
plugin_desc_content = StringIO("""\
|
||||
[Core]
|
||||
Name = Bla{0}Bli
|
||||
Module = SimplePlugin
|
||||
""".format(PLUGIN_NAME_FORBIDEN_STRING))
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf",
|
||||
("yapsy-plugin"))
|
||||
res = analyzer._extractCorePluginInfo("bla",plugin_desc_content)
|
||||
self.assertEqual((None, None), res)
|
||||
plugin_desc_content = StringIO("""\
|
||||
[Core]
|
||||
Name = Simple Plugin
|
||||
""")
|
||||
analyzer = PluginFileAnalyzerWithInfoFile("mouf",
|
||||
("yapsy-plugin"))
|
||||
res = analyzer._extractCorePluginInfo("bla",plugin_desc_content)
|
||||
self.assertEqual((None, None), res)
|
||||
plugin_desc_content = StringIO("""\
|
||||
[Core]
|
||||
Module = Simple Plugin
|
||||
""")
|
||||
res = analyzer._extractCorePluginInfo("bla",plugin_desc_content)
|
||||
self.assertEqual((None, None), res)
|
||||
plugin_desc_content = StringIO("""\
|
||||
[Mouf]
|
||||
Bla = Simple Plugin
|
||||
""")
|
||||
res = analyzer._extractCorePluginInfo("bla",plugin_desc_content)
|
||||
self.assertEqual((None, None), res)
|
||||
|
||||
|
||||
class PluginFileAnalyzerMathingRegexTest(unittest.TestCase):
|
||||
"""
|
||||
Test that the "regex" analyzer enforces the correct policy.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
self.plugin_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"plugins")
|
||||
self.yapsy_plugin_path = os.path.join(self.plugin_directory,"SimplePlugin.py")
|
||||
self.version_plugin_10_path = os.path.join(self.plugin_directory,"VersionedPlugin10.py")
|
||||
self.version_plugin_12_path = os.path.join(self.plugin_directory,"VersionedPlugin12.py")
|
||||
|
||||
def test_Contruction(self):
|
||||
analyzer = PluginFileAnalyzerMathingRegex("mouf",".*")
|
||||
self.assertEqual(analyzer.name,"mouf")
|
||||
|
||||
def test_isValid(self):
|
||||
analyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
self.assertFalse(analyzer.isValidPlugin(self.yapsy_plugin_path))
|
||||
self.assertTrue(analyzer.isValidPlugin(self.version_plugin_10_path))
|
||||
self.assertTrue(analyzer.isValidPlugin(self.version_plugin_12_path))
|
||||
|
||||
def test_getInfosDictFromPlugin(self):
|
||||
analyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
info_dict,cf_parser = analyzer.getInfosDictFromPlugin(self.plugin_directory,
|
||||
os.path.basename(self.version_plugin_10_path))
|
||||
self.assertEqual(info_dict,{'path': self.version_plugin_10_path, 'name': 'VersionedPlugin10'})
|
||||
self.assertTrue(isinstance(cf_parser,ConfigParser))
|
||||
|
||||
class PluginFileLocatorTest(unittest.TestCase):
|
||||
"""
|
||||
Test that the "file" locator.
|
||||
|
||||
NB: backward compatible methods are not directly tested here. We
|
||||
rely only on the 'indirect' tests made for the classes that still
|
||||
depend on them.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
self.plugin_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"plugins")
|
||||
self.plugin_as_dir_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"pluginsasdirs")
|
||||
self.plugin_info_file = "simpleplugin.yapsy-plugin"
|
||||
self.plugin_name = "SimplePlugin"
|
||||
self.plugin_impl_file = self.plugin_name+".py"
|
||||
|
||||
def test_default_plugins_place_is_parent_dir(self):
|
||||
"""Test a non-trivial default behaviour introduced some time ago :S"""
|
||||
pl = PluginFileLocator()
|
||||
expected_yapsy_module_path = os.path.dirname(yapsy.__file__)
|
||||
first_plugin_place = pl.plugins_places[0]
|
||||
self.assertEqual(expected_yapsy_module_path, first_plugin_place)
|
||||
|
||||
def test_given_string_as_plugin_places_raises_error(self):
|
||||
pl = PluginFileLocator()
|
||||
self.assertRaises(ValueError, pl.setPluginPlaces, "/mouf")
|
||||
|
||||
def test_locatePlugins(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(self.plugin_directory,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(self.plugin_directory,self.plugin_name),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
|
||||
def test_locatePlugins_when_plugin_is_symlinked(self):
|
||||
if sys.platform.startswith("win"):
|
||||
return
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
plugin_info_file = "simpleplugin.yapsy-plugin"
|
||||
plugin_impl_file = "SimplePlugin.py"
|
||||
os.symlink(os.path.join(self.plugin_directory,plugin_info_file),
|
||||
os.path.join(temp_dir,plugin_info_file))
|
||||
os.symlink(os.path.join(self.plugin_directory,plugin_impl_file),
|
||||
os.path.join(temp_dir,plugin_impl_file))
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(temp_dir,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(temp_dir,self.plugin_name),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_locatePlugins_when_plugin_is_a_directory(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_as_dir_directory])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(self.plugin_as_dir_directory,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(self.plugin_as_dir_directory,self.plugin_name,
|
||||
"__init__"),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
|
||||
def test_locatePlugins_when_plugin_is_a_symlinked_directory(self):
|
||||
if sys.platform.startswith("win"):
|
||||
return
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
plugin_info_file = "simpleplugin.yapsy-plugin"
|
||||
plugin_impl_dir = "SimplePlugin"
|
||||
os.symlink(os.path.join(self.plugin_as_dir_directory,plugin_info_file),
|
||||
os.path.join(temp_dir,plugin_info_file))
|
||||
os.symlink(os.path.join(self.plugin_as_dir_directory,plugin_impl_dir),
|
||||
os.path.join(temp_dir,plugin_impl_dir))
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(temp_dir,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(temp_dir,self.plugin_name,"__init__"),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_locatePlugins_recursively_when_plugin_is_a_directory(self):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
temp_sub_dir = os.path.join(temp_dir,"plugins")
|
||||
shutil.copytree(self.plugin_as_dir_directory,temp_sub_dir)
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_name,
|
||||
"__init__"),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_locatePlugins_recursively_fails_when_recursion_is_disabled(self):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
temp_sub_dir = os.path.join(temp_dir,"plugins")
|
||||
shutil.copytree(self.plugin_as_dir_directory,temp_sub_dir)
|
||||
pl = PluginFileLocator()
|
||||
pl.disableRecursiveScan()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,0)
|
||||
self.assertEqual(len(candidates),num)
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_locatePlugins_recursively_when_plugin_is_a_symlinked_directory(self):
|
||||
if sys.platform.startswith("win"):
|
||||
return
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
temp_sub_dir = os.path.join(temp_dir,"plugins")
|
||||
os.mkdir(temp_sub_dir)
|
||||
plugin_info_file = "simpleplugin.yapsy-plugin"
|
||||
plugin_impl_dir = "SimplePlugin"
|
||||
os.symlink(os.path.join(self.plugin_as_dir_directory,plugin_info_file),
|
||||
os.path.join(temp_sub_dir,plugin_info_file))
|
||||
os.symlink(os.path.join(self.plugin_as_dir_directory,plugin_impl_dir),
|
||||
os.path.join(temp_sub_dir,plugin_impl_dir))
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_name,
|
||||
"__init__"),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_locatePlugins_recursively_when_plugin_parent_dir_is_a_symlinked_directory(self):
|
||||
if sys.platform.startswith("win"):
|
||||
return
|
||||
# This actually reproduced the "Plugin detection doesn't follow symlinks" bug
|
||||
# at http://sourceforge.net/p/yapsy/bugs/19/
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
temp_sub_dir = os.path.join(temp_dir,"plugins")
|
||||
os.symlink(self.plugin_as_dir_directory,temp_sub_dir)
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([temp_dir])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,1)
|
||||
self.assertEqual(len(candidates),num)
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_info_file),
|
||||
candidates[0][0])
|
||||
self.assertEqual(os.path.join(temp_sub_dir,self.plugin_name,
|
||||
"__init__"),
|
||||
candidates[0][1])
|
||||
self.assertTrue(isinstance(candidates[0][2],PluginInfo))
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_gatherCorePluginInfo(self):
|
||||
pl = PluginFileLocator()
|
||||
plugin_info,cf_parser = pl.gatherCorePluginInfo(self.plugin_directory,"simpleplugin.yapsy-plugin")
|
||||
self.assertTrue(plugin_info.name,"Simple Plugin")
|
||||
self.assertTrue(isinstance(cf_parser,ConfigParser))
|
||||
plugin_info,cf_parser = pl.gatherCorePluginInfo(self.plugin_directory,"notaplugin.atall")
|
||||
self.assertEqual(plugin_info,None)
|
||||
self.assertEqual(cf_parser,None)
|
||||
|
||||
def test_setAnalyzer(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
newAnalyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
pl.setAnalyzers([newAnalyzer])
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,4)
|
||||
self.assertEqual(len(candidates),num)
|
||||
|
||||
def test_appendAnalyzer(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
newAnalyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
pl.appendAnalyzer(newAnalyzer)
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,5)
|
||||
self.assertEqual(len(candidates),num)
|
||||
|
||||
def test_removeAnalyzers_when_analyzer_is_unknown(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
pl.removeAnalyzers("nogo")
|
||||
|
||||
def test_removeAnalyzers(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
newAnalyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
pl.appendAnalyzer(newAnalyzer)
|
||||
pl.removeAnalyzers("info_ext")
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,4)
|
||||
self.assertEqual(len(candidates),num)
|
||||
|
||||
def test_removeAllAnalyzers(self):
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
pl.removeAllAnalyzer()
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,0)
|
||||
self.assertEqual(len(candidates),num)
|
||||
|
||||
def test_setPluginInfoClass_for_named_analyzer(self):
|
||||
class SpecificPluginInfo(PluginInfo):
|
||||
pass
|
||||
pl = PluginFileLocator()
|
||||
pl.setPluginPlaces([self.plugin_directory])
|
||||
newAnalyzer = PluginFileAnalyzerMathingRegex("mouf",r".*VersionedPlugin\d+\.py$")
|
||||
pl.appendAnalyzer(newAnalyzer)
|
||||
pl.setPluginInfoClass(SpecificPluginInfo,"info_ext")
|
||||
candidates, num = pl.locatePlugins()
|
||||
self.assertEqual(num,5)
|
||||
self.assertEqual(len(candidates),num)
|
||||
versioned_plugins = [c for c in candidates if "VersionedPlugin" in c[0]]
|
||||
self.assertEqual(4,len(versioned_plugins))
|
||||
for p in versioned_plugins:
|
||||
self.assertTrue(isinstance(p[2],PluginInfo))
|
||||
simple_plugins = [c for c in candidates if "VersionedPlugin" not in c[0]]
|
||||
self.assertEqual(1,len(simple_plugins))
|
||||
for p in simple_plugins:
|
||||
self.assertTrue(isinstance(p[2],SpecificPluginInfo))
|
||||
|
||||
|
||||
class PluginManagerSetUpTest(unittest.TestCase):
|
||||
|
||||
def test_default_init(self):
|
||||
pm = PluginManager()
|
||||
self.assertEqual(["Default"],pm.getCategories())
|
||||
self.assertTrue(isinstance(pm.getPluginLocator(),PluginFileLocator))
|
||||
|
||||
def test_init_with_category_filter(self):
|
||||
pm = PluginManager(categories_filter={"Mouf": IPlugin})
|
||||
self.assertEqual(["Mouf"],pm.getCategories())
|
||||
self.assertTrue(isinstance(pm.getPluginLocator(),PluginFileLocator))
|
||||
|
||||
def test_init_with_plugin_info_ext(self):
|
||||
pm = PluginManager(plugin_info_ext="bla")
|
||||
self.assertEqual(["Default"],pm.getCategories())
|
||||
self.assertTrue(isinstance(pm.getPluginLocator(),PluginFileLocator))
|
||||
|
||||
def test_init_with_plugin_locator(self):
|
||||
class SpecificLocator(IPluginLocator):
|
||||
pass
|
||||
pm = PluginManager(plugin_locator=SpecificLocator())
|
||||
self.assertEqual(["Default"],pm.getCategories())
|
||||
self.assertTrue(isinstance(pm.getPluginLocator(),SpecificLocator))
|
||||
|
||||
def test_init_with_plugin_info_ext_and_locator(self):
|
||||
class SpecificLocator(IPluginLocator):
|
||||
pass
|
||||
self.assertRaises(ValueError,
|
||||
PluginManager,plugin_info_ext="bla",
|
||||
plugin_locator=SpecificLocator())
|
||||
|
||||
def test_updatePluginPlaces(self):
|
||||
class SpecificLocator(IPluginLocator):
|
||||
pass
|
||||
pm = PluginManager()
|
||||
pm.setPluginPlaces(["bla/bli"])
|
||||
pm.updatePluginPlaces(["mif/maf"])
|
||||
self.assertEqual(set(["bla/bli","mif/maf"]),set(pm.getPluginLocator().plugins_places))
|
||||
|
||||
def test_getPluginCandidates_too_early(self):
|
||||
pm = PluginManager()
|
||||
self.assertRaises(RuntimeError,pm.getPluginCandidates)
|
||||
|
||||
def test_setPluginLocator_with_plugin_info_class(self):
|
||||
class SpecificLocator(IPluginLocator):
|
||||
|
||||
def getPluginInfoClass(self):
|
||||
return self.picls
|
||||
|
||||
def setPluginInfoClass(self,picls):
|
||||
self.picls = picls
|
||||
|
||||
class SpecificPluginInfo(PluginInfo):
|
||||
pass
|
||||
pm = PluginManager()
|
||||
pm.setPluginLocator(SpecificLocator(),picls=SpecificPluginInfo)
|
||||
self.assertEqual(SpecificPluginInfo,pm.getPluginInfoClass())
|
||||
|
||||
def test_setPluginLocator_with_invalid_locator(self):
|
||||
class SpecificLocator:
|
||||
pass
|
||||
pm = PluginManager()
|
||||
self.assertRaises(TypeError,
|
||||
pm.setPluginLocator,SpecificLocator())
|
||||
|
||||
def test_setPluginInfoClass_with_strategies(self):
|
||||
class SpecificPluginInfo(PluginInfo):
|
||||
pass
|
||||
class SpecificLocator(IPluginLocator):
|
||||
def setPluginInfoClass(self,cls,name):
|
||||
if not hasattr(self,"icls"):
|
||||
self.icls = {}
|
||||
self.icls[name] = cls
|
||||
loc = SpecificLocator()
|
||||
pm = PluginManager(plugin_locator=loc)
|
||||
pm.setPluginInfoClass(SpecificPluginInfo,["mouf","hop"])
|
||||
self.assertEqual({"mouf":SpecificPluginInfo,"hop":SpecificPluginInfo},loc.icls)
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(IPluginLocatorTest),
|
||||
unittest.TestLoader().loadTestsFromTestCase(PluginFileAnalyzerWithInfoFileTest),
|
||||
unittest.TestLoader().loadTestsFromTestCase(PluginFileAnalyzerMathingRegexTest),
|
||||
unittest.TestLoader().loadTestsFromTestCase(PluginFileLocatorTest),
|
||||
unittest.TestLoader().loadTestsFromTestCase(PluginManagerSetUpTest),
|
||||
])
|
52
test/test_PluginInfo.py
Normal file
52
test/test_PluginInfo.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
import test_settings
|
||||
from yapsy.compat import ConfigParser
|
||||
import unittest
|
||||
|
||||
|
||||
from yapsy.PluginInfo import PluginInfo
|
||||
|
||||
|
||||
class PluginInfoTest(unittest.TestCase):
|
||||
"""
|
||||
Test basic manipulations of PluginInfo.
|
||||
"""
|
||||
|
||||
def testDefaultValuesAndAccessors(self):
|
||||
pi = PluginInfo("mouf","/bla/mouf")
|
||||
self.assertEqual("mouf",pi.name)
|
||||
self.assertEqual("/bla/mouf",pi.path)
|
||||
self.assertEqual(None,pi.plugin_object)
|
||||
self.assertEqual([],pi.categories)
|
||||
self.assertEqual(None,pi.error)
|
||||
self.assertEqual("0.0",pi.version)
|
||||
self.assertEqual("Unknown",pi.author)
|
||||
self.assertEqual("Unknown",pi.copyright)
|
||||
self.assertEqual("None",pi.website)
|
||||
self.assertEqual("",pi.description)
|
||||
self.assertEqual("UnknownCategory",pi.category)
|
||||
|
||||
def testDetailsAccessors(self):
|
||||
pi = PluginInfo("mouf","/bla/mouf")
|
||||
details = ConfigParser()
|
||||
details.add_section("Core")
|
||||
details.set("Core","Name","hop")
|
||||
details.set("Core","Module","/greuh")
|
||||
details.add_section("Documentation")
|
||||
details.set("Documentation","Author","me")
|
||||
pi.details = details
|
||||
# Beware this is not so obvious: the plugin info still points
|
||||
# (and possibly modifies) the same instance of ConfigParser
|
||||
self.assertEqual(details,pi.details)
|
||||
# also the name and path are kept to their original value when
|
||||
# the details is set in one go.
|
||||
self.assertEqual("mouf",pi.name)
|
||||
self.assertEqual("/bla/mouf",pi.path)
|
||||
# check that some other info do change...
|
||||
self.assertEqual("me",pi.author)
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(PluginInfoTest),
|
||||
])
|
46
test/test_SimpleMultiprocessPlugin.py
Normal file
46
test/test_SimpleMultiprocessPlugin.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from yapsy.MultiprocessPluginManager import MultiprocessPluginManager
|
||||
|
||||
class SimpleMultiprocessTestCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading of a multiprocessed plugin as well as basic
|
||||
communication.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
self.mpPluginManager = MultiprocessPluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="multiprocess-plugin")
|
||||
# load the plugins that may be found
|
||||
self.mpPluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def testUpAndRunning(self):
|
||||
"""
|
||||
Test if the plugin is loaded and if the communication pipe is properly setuped.
|
||||
"""
|
||||
for plugin_index, plugin in enumerate(self.mpPluginManager.getAllPlugins()):
|
||||
child_pipe = plugin.plugin_object.child_pipe
|
||||
content_from_parent = "hello-{0}-from-parent".format(plugin_index)
|
||||
child_pipe.send(content_from_parent)
|
||||
content_from_child = False
|
||||
if child_pipe.poll(5):
|
||||
content_from_child = child_pipe.recv()
|
||||
self.assertEqual("{0}|echo_from_child".format(content_from_parent),
|
||||
content_from_child)
|
||||
num_tested_plugin = plugin_index+1
|
||||
self.assertEqual(2, num_tested_plugin)
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(SimpleMultiprocessTestCase),
|
||||
])
|
386
test/test_SimplePlugin.py
Normal file
386
test/test_SimplePlugin.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from yapsy.PluginManager import PluginManager
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.PluginFileLocator import PluginFileLocator
|
||||
from yapsy.PluginFileLocator import IPluginFileAnalyzer
|
||||
from yapsy import NormalizePluginNameForModuleName
|
||||
from yapsy.compat import ConfigParser
|
||||
|
||||
class YapsyUtils(unittest.TestCase):
|
||||
|
||||
def test_NormalizePluginNameForModuleName_on_ok_name(self):
|
||||
self.assertEqual("moufGlop2",NormalizePluginNameForModuleName("moufGlop2"))
|
||||
|
||||
def test_NormalizePluginNameForModuleName_on_empty_name(self):
|
||||
self.assertEqual("_",NormalizePluginNameForModuleName(""))
|
||||
|
||||
def test_NormalizePluginNameForModuleName_on_name_with_space(self):
|
||||
self.assertEqual("mouf_glop",NormalizePluginNameForModuleName("mouf glop"))
|
||||
|
||||
def test_NormalizePluginNameForModuleName_on_name_with_nonalphanum(self):
|
||||
self.assertEqual("mouf__glop_a_é",NormalizePluginNameForModuleName("mouf+?glop:a/é"))
|
||||
|
||||
|
||||
|
||||
class SimpleTestCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading of a simple plugin as well as basic
|
||||
commands.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
self.simplePluginManager = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# load the plugins that may be found
|
||||
self.simplePluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.simplePluginManager.getCategories()),1)
|
||||
sole_category = self.simplePluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(self.simplePluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = self.simplePluginManager.getPluginsOfCategory(sole_category)[0]
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,"Simple Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
|
||||
def testGetAll(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertEqual(len(self.simplePluginManager.getAllPlugins()),1)
|
||||
self.assertEqual(self.simplePluginManager.getAllPlugins()[0],self.plugin_info)
|
||||
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
self.simplePluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.simplePluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
class SimplePluginAdvancedManipulationTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test some advanced manipulation on the core data of a PluginManager.
|
||||
"""
|
||||
|
||||
|
||||
def testCategoryManipulation(self):
|
||||
"""
|
||||
Test querying, removing and adding plugins from/to a category.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1)
|
||||
plugin_info = spm.getPluginsOfCategory(sole_category)[0]
|
||||
# try to remove it and check that is worked
|
||||
spm.removePluginFromCategory(plugin_info,sole_category)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0)
|
||||
# now re-add this plugin the to same category
|
||||
spm.appendPluginToCategory(plugin_info,sole_category)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1)
|
||||
|
||||
|
||||
def testChangingCategoriesFilter(self):
|
||||
"""
|
||||
Test the effect of setting a new category filer.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
newCategory = "Mouf"
|
||||
# Pre-requisite for the test
|
||||
previousCategories = spm.getCategories()
|
||||
self.assertTrue(len(previousCategories) >= 1)
|
||||
self.assertTrue(newCategory not in previousCategories)
|
||||
# change the category and see what's happening
|
||||
spm.setCategoriesFilter({newCategory: IPlugin})
|
||||
spm.collectPlugins()
|
||||
for categoryName in previousCategories:
|
||||
self.assertRaises(KeyError, spm.getPluginsOfCategory, categoryName)
|
||||
self.assertTrue(len(spm.getPluginsOfCategory(newCategory)) >= 1)
|
||||
|
||||
|
||||
def testCandidatesManipulation(self):
|
||||
"""
|
||||
Test querying, removing and adding plugins from/to the lkist
|
||||
of plugins to load.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# locate the plugins that should be loaded
|
||||
spm.locatePlugins()
|
||||
# check nb of candidatesx
|
||||
self.assertEqual(len(spm.getPluginCandidates()),1)
|
||||
# get the description of the plugin candidate
|
||||
candidate = spm.getPluginCandidates()[0]
|
||||
self.assertTrue(isinstance(candidate,tuple))
|
||||
# try removing the candidate
|
||||
spm.removePluginCandidate(candidate)
|
||||
self.assertEqual(len(spm.getPluginCandidates()),0)
|
||||
# try re-adding it
|
||||
spm.appendPluginCandidate(candidate)
|
||||
self.assertEqual(len(spm.getPluginCandidates()),1)
|
||||
|
||||
def testTwoStepsLoad(self):
|
||||
"""
|
||||
Test loading the plugins in two steps in order to collect more
|
||||
deltailed informations.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# trigger the first step to look up for plugins
|
||||
spm.locatePlugins()
|
||||
# make full use of the "feedback" the loadPlugins can give
|
||||
# - set-up the callback function that will be called *before*
|
||||
# loading each plugin
|
||||
callback_infos = []
|
||||
def preload_cbk(plugin_info):
|
||||
callback_infos.append(plugin_info)
|
||||
callback_after_infos = []
|
||||
def postload_cbk(plugin_info):
|
||||
callback_after_infos.append(plugin_info)
|
||||
# - gather infos about the processed plugins (loaded or not)
|
||||
loadedPlugins = spm.loadPlugins(callback=preload_cbk, callback_after=postload_cbk)
|
||||
self.assertEqual(len(loadedPlugins),1)
|
||||
self.assertEqual(len(callback_infos),1)
|
||||
self.assertEqual(loadedPlugins[0].error,None)
|
||||
self.assertEqual(loadedPlugins[0],callback_infos[0])
|
||||
self.assertEqual(len(callback_after_infos),1)
|
||||
self.assertEqual(loadedPlugins[0],callback_infos[0])
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1)
|
||||
plugin_info = spm.getPluginsOfCategory(sole_category)[0]
|
||||
# try to remove it and check that is worked
|
||||
spm.removePluginFromCategory(plugin_info,sole_category)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0)
|
||||
# now re-add this plugin the to same category
|
||||
spm.appendPluginToCategory(plugin_info,sole_category)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1)
|
||||
|
||||
def testMultipleCategoriesForASamePlugin(self):
|
||||
"""
|
||||
Test that associating a plugin to multiple categories works as expected.
|
||||
"""
|
||||
class AnotherPluginIfce(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
def activate(self):
|
||||
pass
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
spm = PluginManager(
|
||||
categories_filter = {
|
||||
"Default": IPlugin,
|
||||
"IP": IPlugin,
|
||||
"Other": AnotherPluginIfce,
|
||||
},
|
||||
directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),3)
|
||||
categories = spm.getCategories()
|
||||
self.assertTrue("Default" in categories)
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("Default")), 1)
|
||||
plugin_info = spm.getPluginsOfCategory("Default")[0]
|
||||
self.assertTrue("Default" in plugin_info.categories)
|
||||
self.assertTrue("IP" in plugin_info.categories)
|
||||
self.assertTrue("IP" in categories)
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("IP")),1)
|
||||
self.assertTrue("Other" in categories)
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("Other")),0)
|
||||
# try to remove the plugin from one category and check the
|
||||
# other category
|
||||
spm.removePluginFromCategory(plugin_info, "Default")
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("Default")), 0)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("IP")), 1)
|
||||
# now re-add this plugin the to same category
|
||||
spm.appendPluginToCategory(plugin_info, "Default")
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("Default")),1)
|
||||
self.assertEqual(len(spm.getPluginsOfCategory("IP")),1)
|
||||
|
||||
def testGetPluginOf(self):
|
||||
"""
|
||||
Test the plugin query function.
|
||||
"""
|
||||
spm = PluginManager(
|
||||
categories_filter = {
|
||||
"Default": IPlugin,
|
||||
"IP": IPlugin,
|
||||
},
|
||||
directories_list=[
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOf(categories="IP")), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(categories="Default")), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(name="Simple Plugin")), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(is_activated=False)), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(categories="IP", is_activated=True)), 0)
|
||||
self.assertEqual(len(spm.getPluginsOf(categories="IP", is_activated=False)), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(categories="IP", pouet=False)), 0)
|
||||
self.assertEqual(len(spm.getPluginsOf(categories=["IP"])), 0)
|
||||
# The order in the categories are added to plugin info is random in this setup, hence the strange formula below
|
||||
self.assertEqual(len(spm.getPluginsOf(categories=["IP", "Default"]) | spm.getPluginsOf(categories=["Default", "IP"])), 1)
|
||||
self.assertEqual(len(spm.getPluginsOf(category="Default") | spm.getPluginsOf(category="IP")), 1)
|
||||
|
||||
class SimplePluginDetectionTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test particular aspects of plugin detection
|
||||
"""
|
||||
|
||||
def testRecursivePluginlocation(self):
|
||||
"""
|
||||
Test detection of plugins which by default must be
|
||||
recursive. Here we give the test directory as a plugin place
|
||||
whereas we expect the plugins to be in test/plugins.
|
||||
"""
|
||||
spm = PluginManager(directories_list=[
|
||||
os.path.dirname(os.path.abspath(__file__))])
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),2)
|
||||
|
||||
def testDisablingRecursivePluginLocationIsEnforced(self):
|
||||
"""
|
||||
Test detection of plugins when the detection is non recursive.
|
||||
Here we test that it cannot look into subdirectories of the
|
||||
test directory.
|
||||
"""
|
||||
pluginLocator = PluginFileLocator()
|
||||
pluginLocator.setPluginPlaces([
|
||||
os.path.dirname(os.path.abspath(__file__))])
|
||||
pluginLocator.disableRecursiveScan()
|
||||
spm = PluginManager()
|
||||
spm.setPluginLocator(pluginLocator)
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0)
|
||||
|
||||
|
||||
def testDisablingRecursivePluginLocationAllowsFindingTopLevelPlugins(self):
|
||||
"""
|
||||
Test detection of plugins when the detection is non
|
||||
recursive. Here we test that if we give test/plugin as the
|
||||
directory to scan it can find the plugin.
|
||||
"""
|
||||
pluginLocator = PluginFileLocator()
|
||||
pluginLocator.setPluginPlaces([
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")])
|
||||
pluginLocator.disableRecursiveScan()
|
||||
spm = PluginManager()
|
||||
spm.setPluginLocator(pluginLocator)
|
||||
# load the plugins that may be found
|
||||
spm.collectPlugins()
|
||||
# check that the getCategories works
|
||||
self.assertEqual(len(spm.getCategories()),1)
|
||||
sole_category = spm.getCategories()[0]
|
||||
# check the getPluginsOfCategory
|
||||
self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1)
|
||||
|
||||
def testEnforcingPluginDirsDoesNotKeepDefaultDir(self):
|
||||
"""
|
||||
Test that providing the directories list override the default search directory
|
||||
instead of extending the default list.
|
||||
"""
|
||||
|
||||
class AcceptAllPluginFileAnalyzer(IPluginFileAnalyzer):
|
||||
|
||||
def __init__(self):
|
||||
IPluginFileAnalyzer.__init__(self, "AcceptAll")
|
||||
|
||||
def isValidPlugin(self, filename):
|
||||
return True
|
||||
|
||||
def getInfosDictFromPlugin(self, dirpath, filename):
|
||||
return { "name": filename, "path": dirpath}, ConfigParser()
|
||||
|
||||
pluginLocator = PluginFileLocator()
|
||||
pluginLocator.setAnalyzers([AcceptAllPluginFileAnalyzer()])
|
||||
|
||||
spm_default_dirs = PluginManager(plugin_locator= pluginLocator)
|
||||
spm_default_dirs.locatePlugins()
|
||||
candidates_in_default_dir = spm_default_dirs.getPluginCandidates()
|
||||
candidates_files_in_default_dir = set([c[0] for c in candidates_in_default_dir])
|
||||
|
||||
pluginLocator = PluginFileLocator()
|
||||
pluginLocator.setAnalyzers([AcceptAllPluginFileAnalyzer()])
|
||||
spm = PluginManager(plugin_locator= pluginLocator,
|
||||
directories_list=[os.path.dirname(os.path.abspath(__file__)),"does-not-exists"])
|
||||
spm.locatePlugins()
|
||||
candidates = spm.getPluginCandidates()
|
||||
candidates_files = set([c[0] for c in candidates])
|
||||
|
||||
self.assertFalse(set(candidates_files_in_default_dir).issubset(set(candidates_files)))
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(YapsyUtils),
|
||||
unittest.TestLoader().loadTestsFromTestCase(SimpleTestCase),
|
||||
unittest.TestLoader().loadTestsFromTestCase(SimplePluginAdvancedManipulationTestsCase),
|
||||
unittest.TestLoader().loadTestsFromTestCase(SimplePluginDetectionTestsCase),
|
||||
])
|
135
test/test_Singleton.py
Normal file
135
test/test_Singleton.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
|
||||
from yapsy.VersionedPluginManager import VersionedPluginManager
|
||||
from yapsy.PluginManager import PluginManagerSingleton
|
||||
from yapsy.compat import ConfigParser
|
||||
|
||||
|
||||
"""
|
||||
There can be only one series of tests for the singleton, guess why ...
|
||||
"""
|
||||
|
||||
class ConfigSingletonTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading of a simple plugin as well as basic
|
||||
commands, use the Singleton version of the ConfigurablePluginManager.
|
||||
"""
|
||||
|
||||
CONFIG_FILE = test_settings.TEMP_CONFIG_FILE_NAME
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create a config file
|
||||
self.config_file = self.CONFIG_FILE
|
||||
self.config_parser = ConfigParser()
|
||||
self.plugin_info = None
|
||||
|
||||
# create the plugin manager
|
||||
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager,VersionedPluginManager])
|
||||
pluginManager = PluginManagerSingleton.get()
|
||||
pluginManager.setPluginPlaces(directories_list=[os.path.dirname(os.path.abspath(__file__))])
|
||||
pluginManager.setPluginInfoExtension("yapsy-config-plugin")
|
||||
pluginManager.setConfigParser(self.config_parser,self.update_config)
|
||||
# load the plugins that may be found
|
||||
pluginManager.collectPlugins()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
When the test has been performed erase the temp file.
|
||||
"""
|
||||
if os.path.isfile(self.config_file):
|
||||
os.remove(self.config_file)
|
||||
|
||||
|
||||
def testConfigurationFileExistence(self):
|
||||
"""
|
||||
Test if the configuration file has been properly written.
|
||||
"""
|
||||
# activate the only loaded plugin
|
||||
self.plugin_activate()
|
||||
# get rid of the plugin manager and create a new one
|
||||
self.config_parser.read(self.config_file)
|
||||
self.assertTrue(self.config_parser.has_section("Plugin Management"))
|
||||
self.assertTrue(self.config_parser.has_option("Plugin Management",
|
||||
"default_plugins_to_load"))
|
||||
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation/deactivaion procedures work.
|
||||
"""
|
||||
self.plugin_activate()
|
||||
PluginManagerSingleton.get().deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
def testPluginOptions(self):
|
||||
"""
|
||||
Test is the plugin can register and access options from the
|
||||
ConfigParser.
|
||||
"""
|
||||
self.plugin_activate()
|
||||
plugin = self.plugin_info.plugin_object
|
||||
plugin.choseTestOption("voila")
|
||||
self.assertTrue(plugin.checkTestOption())
|
||||
self.assertEqual(plugin.getTestOption(),"voila")
|
||||
|
||||
|
||||
#--- UTILITIES
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
pluginManager = PluginManagerSingleton.get()
|
||||
# check nb of categories
|
||||
self.assertEqual(len(pluginManager.getCategories()),1)
|
||||
sole_category = pluginManager.getCategories()[0]
|
||||
# check the number of plugins
|
||||
self.assertEqual(len(pluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = pluginManager.getPluginsOfCategory(sole_category)[0]
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,"Config Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def plugin_activate(self):
|
||||
"""
|
||||
Activate the plugin with basic checking
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
if not self.plugin_info.plugin_object.is_activated:
|
||||
PluginManagerSingleton.get().activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
def update_config(self):
|
||||
"""
|
||||
Write the content of the ConfigParser in a file.
|
||||
"""
|
||||
cf = open(self.config_file,"a")
|
||||
self.config_parser.write(cf)
|
||||
cf.close()
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(ConfigSingletonTestsCase),
|
||||
])
|
133
test/test_VersionedPlugin.py
Normal file
133
test/test_VersionedPlugin.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
from . import test_settings
|
||||
from .test_settings import TEST_MESSAGE
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.VersionedPluginManager import VersionedPluginManager
|
||||
|
||||
|
||||
class VersionedTestsCase(unittest.TestCase):
|
||||
"""
|
||||
Test the correct loading of a simple plugin as well as basic
|
||||
commands.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
init
|
||||
"""
|
||||
# create the plugin manager
|
||||
self.versionedPluginManager = VersionedPluginManager(
|
||||
directories_list=[os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),"plugins")],
|
||||
plugin_info_ext="version-plugin",
|
||||
)
|
||||
# load the plugins that may be found
|
||||
self.versionedPluginManager.collectPlugins()
|
||||
# Will be used later
|
||||
self.plugin_info = None
|
||||
|
||||
def plugin_loading_check(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
if self.plugin_info is None:
|
||||
# check nb of categories
|
||||
self.assertEqual(len(self.versionedPluginManager.getCategories()),1)
|
||||
sole_category = self.versionedPluginManager.getCategories()[0]
|
||||
# check the number of plugins (the older versions of the
|
||||
# plugins should not be there)
|
||||
self.assertEqual(len(self.versionedPluginManager.getPluginsOfCategory(sole_category)),1)
|
||||
# older versions of the plugin should be found in the attic
|
||||
self.assertEqual(len(self.versionedPluginManager.getPluginsOfCategoryFromAttic(sole_category)),4)
|
||||
plugins = self.versionedPluginManager.getPluginsOfCategory(sole_category)
|
||||
self.plugin_info = None
|
||||
for plugin_info in plugins:
|
||||
TEST_MESSAGE("plugin info: %s" % plugin_info)
|
||||
if plugin_info.name == "Versioned Plugin":
|
||||
self.plugin_info = plugin_info
|
||||
break
|
||||
self.assertTrue(self.plugin_info)
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,"Versioned Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testLoaded(self):
|
||||
"""
|
||||
Test if the correct plugin has been loaded.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
sole_category = self.versionedPluginManager.getCategories()[0]
|
||||
self.assertEqual(len(self.versionedPluginManager.getLatestPluginsOfCategory(sole_category)),1)
|
||||
self.plugin_info = self.versionedPluginManager.getLatestPluginsOfCategory(sole_category)[0]
|
||||
TEST_MESSAGE("plugin info: %s" % self.plugin_info)
|
||||
# test that the name of the plugin has been correctly defined
|
||||
self.assertEqual(self.plugin_info.name,"Versioned Plugin")
|
||||
self.assertEqual(sole_category,self.plugin_info.category)
|
||||
self.assertEqual("1.2",str(self.plugin_info.version))
|
||||
|
||||
|
||||
def testLatestPluginOfCategory(self):
|
||||
self.plugin_loading_check()
|
||||
|
||||
def testActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
self.versionedPluginManager.activatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.versionedPluginManager.deactivatePluginByName(self.plugin_info.name,
|
||||
self.plugin_info.category)
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
# also check that this is the plugin of the latest version
|
||||
# that has been activated (ok the following test is already
|
||||
# ensured by the plugin_loading_check method, but this is to
|
||||
# make the things clear: the plugin chosen for activation is
|
||||
# the one with the latest version)
|
||||
self.assertEqual("1.2",str(self.plugin_info.version))
|
||||
|
||||
|
||||
def testDirectActivationAndDeactivation(self):
|
||||
"""
|
||||
Test if the activation procedure works when directly activating a plugin.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object)
|
||||
self.plugin_info.plugin_object.activate()
|
||||
self.assertTrue(self.plugin_info.plugin_object.is_activated)
|
||||
self.plugin_info.plugin_object.deactivate()
|
||||
self.assertTrue(not self.plugin_info.plugin_object.is_activated)
|
||||
|
||||
|
||||
def testAtticConsistencyAfterCategoryFilterUpdate(self):
|
||||
"""
|
||||
Test that changing the category filer doesn't make the attic inconsistent.
|
||||
"""
|
||||
self.plugin_loading_check()
|
||||
newCategory = "Mouf"
|
||||
# Pre-requisite for the test
|
||||
previousCategories = self.versionedPluginManager.getCategories()
|
||||
self.assertTrue(len(previousCategories) >= 1)
|
||||
self.assertTrue(newCategory not in previousCategories)
|
||||
# change the category and see what's happening
|
||||
self.versionedPluginManager.setCategoriesFilter({newCategory: IPlugin})
|
||||
self.versionedPluginManager.collectPlugins()
|
||||
for categoryName in previousCategories:
|
||||
self.assertRaises(KeyError, self.versionedPluginManager\
|
||||
.getPluginsOfCategory, categoryName)
|
||||
self.assertEqual(len(self.versionedPluginManager\
|
||||
.getPluginsOfCategoryFromAttic(newCategory)),4)
|
||||
|
||||
|
||||
suite = unittest.TestSuite([
|
||||
unittest.TestLoader().loadTestsFromTestCase(VersionedTestsCase),
|
||||
])
|
27
test/test_settings.py
Normal file
27
test/test_settings.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import logging
|
||||
TEST_MESSAGE = logging.debug
|
||||
|
||||
TEMP_CONFIG_FILE_NAME=os.path.join(
|
||||
os.path.dirname(
|
||||
os.path.abspath(__file__)),
|
||||
"tempconfig")
|
||||
|
||||
# set correct loading path for yapsy's files
|
||||
sys.path.insert(0,
|
||||
os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
|
||||
sys.path.insert(0,
|
||||
os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.abspath(__file__)))))
|
||||
|
||||
|
||||
|
207
yapsy/AutoInstallPluginManager.py
Normal file
207
yapsy/AutoInstallPluginManager.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines plugin managers that can handle the installation of plugin
|
||||
files into the right place. Then the end-user does not have to browse
|
||||
to the plugin directory to install them.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.PluginManagerDecorator import PluginManagerDecorator
|
||||
from yapsy import log
|
||||
from yapsy.compat import StringIO, str
|
||||
|
||||
|
||||
class AutoInstallPluginManager(PluginManagerDecorator):
|
||||
"""
|
||||
A plugin manager that also manages the installation of the plugin
|
||||
files into the appropriate directory.
|
||||
|
||||
Ctor Arguments:
|
||||
|
||||
``plugin_install_dir``
|
||||
The directory where new plugins to be installed will be copied.
|
||||
|
||||
.. warning:: If ``plugin_install_dir`` does not correspond to
|
||||
an element of the ``directories_list``, it is
|
||||
appended to the later.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self,
|
||||
plugin_install_dir=None,
|
||||
decorated_manager=None,
|
||||
# The following args will only be used if we need to
|
||||
# create a default PluginManager
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext="yapsy-plugin"):
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default":IPlugin}
|
||||
# Create the base decorator class
|
||||
PluginManagerDecorator.__init__(self,
|
||||
decorated_manager,
|
||||
categories_filter,
|
||||
directories_list,
|
||||
plugin_info_ext)
|
||||
# set the directory for new plugins
|
||||
self.plugins_places=[]
|
||||
self.setInstallDir(plugin_install_dir)
|
||||
|
||||
def setInstallDir(self,plugin_install_dir):
|
||||
"""
|
||||
Set the directory where to install new plugins.
|
||||
"""
|
||||
if not (plugin_install_dir in self.plugins_places):
|
||||
self.plugins_places.append(plugin_install_dir)
|
||||
self.install_dir = plugin_install_dir
|
||||
|
||||
def getInstallDir(self):
|
||||
"""
|
||||
Return the directory where new plugins should be installed.
|
||||
"""
|
||||
return self.install_dir
|
||||
|
||||
def install(self, directory, plugin_info_filename):
|
||||
"""
|
||||
Giving the plugin's info file (e.g. ``myplugin.yapsy-plugin``),
|
||||
and the directory where it is located, get all the files that
|
||||
define the plugin and copy them into the correct directory.
|
||||
|
||||
Return ``True`` if the installation is a success, ``False`` if
|
||||
it is a failure.
|
||||
"""
|
||||
# start collecting essential info about the new plugin
|
||||
plugin_info, config_parser = self._gatherCorePluginInfo(directory, plugin_info_filename)
|
||||
# now determine the path of the file to execute,
|
||||
# depending on wether the path indicated is a
|
||||
# directory or a file
|
||||
if not (os.path.exists(plugin_info.path) or os.path.exists(plugin_info.path+".py") ):
|
||||
log.warning("Could not find the plugin's implementation for %s." % plugin_info.name)
|
||||
return False
|
||||
if os.path.isdir(plugin_info.path):
|
||||
try:
|
||||
shutil.copytree(plugin_info.path,
|
||||
os.path.join(self.install_dir,os.path.basename(plugin_info.path)))
|
||||
shutil.copy(os.path.join(directory, plugin_info_filename),
|
||||
self.install_dir)
|
||||
except:
|
||||
log.error("Could not install plugin: %s." % plugin_info.name)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
elif os.path.isfile(plugin_info.path+".py"):
|
||||
try:
|
||||
shutil.copy(plugin_info.path+".py",
|
||||
self.install_dir)
|
||||
shutil.copy(os.path.join(directory, plugin_info_filename),
|
||||
self.install_dir)
|
||||
except:
|
||||
log.error("Could not install plugin: %s." % plugin_info.name)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def installFromZIP(self, plugin_ZIP_filename):
|
||||
"""
|
||||
Giving the plugin's zip file (e.g. ``myplugin.zip``), check
|
||||
that their is a valid info file in it and correct all the
|
||||
plugin files into the correct directory.
|
||||
|
||||
.. warning:: Only available for python 2.6 and later.
|
||||
|
||||
Return ``True`` if the installation is a success, ``False`` if
|
||||
it is a failure.
|
||||
"""
|
||||
if not os.path.isfile(plugin_ZIP_filename):
|
||||
log.warning("Could not find the plugin's zip file at '%s'." % plugin_ZIP_filename)
|
||||
return False
|
||||
try:
|
||||
candidateZipFile = zipfile.ZipFile(plugin_ZIP_filename)
|
||||
first_bad_file = candidateZipFile.testzip()
|
||||
if first_bad_file:
|
||||
raise Exception("Corrupted ZIP with first bad file '%s'" % first_bad_file)
|
||||
except Exception as e:
|
||||
log.warning("Invalid zip file '%s' (error: %s)." % (plugin_ZIP_filename,e))
|
||||
return False
|
||||
zipContent = candidateZipFile.namelist()
|
||||
log.info("Investigating the content of a zip file containing: '%s'" % zipContent)
|
||||
log.info("Sanity checks on zip's contained files (looking for hazardous path symbols).")
|
||||
# check absence of root path and ".." shortcut that would
|
||||
# send the file oustide the desired directory
|
||||
for containedFileName in zipContent:
|
||||
# WARNING: the sanity checks below are certainly not
|
||||
# exhaustive (maybe we could do something a bit smarter by
|
||||
# using os.path.expanduser, os.path.expandvars and
|
||||
# os.path.normpath)
|
||||
if containedFileName.startswith("/"):
|
||||
log.warning("Unsecure zip file, rejected because one of its file paths ('%s') starts with '/'" % containedFileName)
|
||||
return False
|
||||
if containedFileName.startswith(r"\\") or containedFileName.startswith("//"):
|
||||
log.warning(r"Unsecure zip file, rejected because one of its file paths ('%s') starts with '\\'" % containedFileName)
|
||||
return False
|
||||
if os.path.splitdrive(containedFileName)[0]:
|
||||
log.warning("Unsecure zip file, rejected because one of its file paths ('%s') starts with a drive letter" % containedFileName)
|
||||
return False
|
||||
if os.path.isabs(containedFileName):
|
||||
log.warning("Unsecure zip file, rejected because one of its file paths ('%s') is absolute" % containedFileName)
|
||||
return False
|
||||
pathComponent = os.path.split(containedFileName)
|
||||
if ".." in pathComponent:
|
||||
log.warning("Unsecure zip file, rejected because one of its file paths ('%s') contains '..'" % containedFileName)
|
||||
return False
|
||||
if "~" in pathComponent:
|
||||
log.warning("Unsecure zip file, rejected because one of its file paths ('%s') contains '~'" % containedFileName)
|
||||
return False
|
||||
infoFileCandidates = [filename for filename in zipContent if os.path.dirname(filename)==""]
|
||||
if not infoFileCandidates:
|
||||
log.warning("Zip file structure seems wrong in '%s', no info file found." % plugin_ZIP_filename)
|
||||
return False
|
||||
isValid = False
|
||||
log.info("Looking for the zipped plugin's info file among '%s'" % infoFileCandidates)
|
||||
for infoFileName in infoFileCandidates:
|
||||
infoFile = candidateZipFile.read(infoFileName)
|
||||
log.info("Assuming the zipped plugin info file to be '%s'" % infoFileName)
|
||||
pluginName,moduleName,_ = self._getPluginNameAndModuleFromStream(StringIO(str(infoFile,encoding="utf-8")))
|
||||
if moduleName is None:
|
||||
continue
|
||||
log.info("Checking existence of the expected module '%s' in the zip file" % moduleName)
|
||||
candidate_module_paths = [
|
||||
moduleName,
|
||||
# Try path consistent with the platform specific one
|
||||
os.path.join(moduleName,"__init__.py"),
|
||||
# Try typical paths (unix and windows)
|
||||
"%s/__init__.py" % moduleName,
|
||||
"%s\\__init__.py" % moduleName
|
||||
]
|
||||
for candidate in candidate_module_paths:
|
||||
if candidate in zipContent:
|
||||
isValid = True
|
||||
break
|
||||
if isValid:
|
||||
break
|
||||
if not isValid:
|
||||
log.warning("Zip file structure seems wrong in '%s', "
|
||||
"could not match info file with the implementation of plugin '%s'." % (plugin_ZIP_filename,pluginName))
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
candidateZipFile.extractall(self.install_dir)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error("Could not install plugin '%s' from zip file '%s' (exception: '%s')." % (pluginName,plugin_ZIP_filename,e))
|
||||
return False
|
||||
|
279
yapsy/ConfigurablePluginManager.py
Normal file
279
yapsy/ConfigurablePluginManager.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines plugin managers that can handle configuration files similar to
|
||||
the ini files manipulated by Python's ConfigParser module.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
|
||||
from yapsy.PluginManagerDecorator import PluginManagerDecorator
|
||||
from yapsy.PluginManager import PLUGIN_NAME_FORBIDEN_STRING
|
||||
|
||||
|
||||
class ConfigurablePluginManager(PluginManagerDecorator):
|
||||
"""
|
||||
A plugin manager that also manages a configuration file.
|
||||
|
||||
The configuration file will be accessed through a ``ConfigParser``
|
||||
derivated object. The file can be used for other purpose by the
|
||||
application using this plugin manager as it will only add a new
|
||||
specific section ``[Plugin Management]`` for itself and also new
|
||||
sections for some plugins that will start with ``[Plugin:...]``
|
||||
(only the plugins that explicitly requires to save configuration
|
||||
options will have this kind of section).
|
||||
|
||||
.. warning:: when giving/building the list of plugins to activate
|
||||
by default, there must not be any space in the list
|
||||
(neither in the names nor in between)
|
||||
|
||||
The ``config_change_trigger`` argument can be used to set a
|
||||
specific method to call when the configuration is
|
||||
altered. This will let the client application manage the way
|
||||
they want the configuration to be updated (e.g. write on file
|
||||
at each change or at precise time intervalls or whatever....)
|
||||
|
||||
.. warning:: when no ``config_change_trigger`` is given and if
|
||||
the provided ``configparser_instance`` doesn't handle it
|
||||
implicitely, recording the changes persistently (ie writing on
|
||||
the config file) won't happen.
|
||||
"""
|
||||
|
||||
CONFIG_SECTION_NAME = "Plugin Management"
|
||||
|
||||
|
||||
def __init__(self,
|
||||
configparser_instance=None,
|
||||
config_change_trigger= lambda :True,
|
||||
decorated_manager=None,
|
||||
# The following args will only be used if we need to
|
||||
# create a default PluginManager
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext="yapsy-plugin"):
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default":IPlugin}
|
||||
# Create the base decorator class
|
||||
PluginManagerDecorator.__init__(self,decorated_manager,
|
||||
categories_filter,
|
||||
directories_list,
|
||||
plugin_info_ext)
|
||||
self.setConfigParser(configparser_instance, config_change_trigger)
|
||||
|
||||
|
||||
def setConfigParser(self,configparser_instance,config_change_trigger):
|
||||
"""
|
||||
Set the ConfigParser instance.
|
||||
"""
|
||||
self.config_parser = configparser_instance
|
||||
# set the (optional) fucntion to be called when the
|
||||
# configuration is changed:
|
||||
self.config_has_changed = config_change_trigger
|
||||
|
||||
def __getCategoryPluginsListFromConfig(self, plugin_list_str):
|
||||
"""
|
||||
Parse the string describing the list of plugins to activate,
|
||||
to discover their actual names and return them.
|
||||
"""
|
||||
return plugin_list_str.strip(" ").split("%s"%PLUGIN_NAME_FORBIDEN_STRING)
|
||||
|
||||
def __getCategoryPluginsConfigFromList(self, plugin_list):
|
||||
"""
|
||||
Compose a string describing the list of plugins to activate
|
||||
"""
|
||||
return PLUGIN_NAME_FORBIDEN_STRING.join(plugin_list)
|
||||
|
||||
def __getCategoryOptionsName(self,category_name):
|
||||
"""
|
||||
Return the appropirately formated version of the category's
|
||||
option.
|
||||
"""
|
||||
return "%s_plugins_to_load" % category_name.replace(" ","_")
|
||||
|
||||
def __addPluginToConfig(self,category_name, plugin_name):
|
||||
"""
|
||||
Utility function to add a plugin to the list of plugin to be
|
||||
activated.
|
||||
"""
|
||||
# check that the section is here
|
||||
if not self.config_parser.has_section(self.CONFIG_SECTION_NAME):
|
||||
self.config_parser.add_section(self.CONFIG_SECTION_NAME)
|
||||
# check that the category's list of activated plugins is here too
|
||||
option_name = self.__getCategoryOptionsName(category_name)
|
||||
if not self.config_parser.has_option(self.CONFIG_SECTION_NAME, option_name):
|
||||
# if there is no list yet add a new one
|
||||
self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,plugin_name)
|
||||
return self.config_has_changed()
|
||||
else:
|
||||
# get the already existing list and append the new
|
||||
# activated plugin to it.
|
||||
past_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME,option_name)
|
||||
past_list = self.__getCategoryPluginsListFromConfig(past_list_str)
|
||||
# make sure we don't add it twice
|
||||
if plugin_name not in past_list:
|
||||
past_list.append(plugin_name)
|
||||
new_list_str = self.__getCategoryPluginsConfigFromList(past_list)
|
||||
self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,new_list_str)
|
||||
return self.config_has_changed()
|
||||
|
||||
def __removePluginFromConfig(self,category_name, plugin_name):
|
||||
"""
|
||||
Utility function to add a plugin to the list of plugin to be
|
||||
activated.
|
||||
"""
|
||||
# check that the section is here
|
||||
if not self.config_parser.has_section(self.CONFIG_SECTION_NAME):
|
||||
# then nothing to remove :)
|
||||
return
|
||||
# check that the category's list of activated plugins is here too
|
||||
option_name = self.__getCategoryOptionsName(category_name)
|
||||
if not self.config_parser.has_option(self.CONFIG_SECTION_NAME, option_name):
|
||||
# if there is no list still nothing to do
|
||||
return
|
||||
else:
|
||||
# get the already existing list
|
||||
past_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME,option_name)
|
||||
past_list = self.__getCategoryPluginsListFromConfig(past_list_str)
|
||||
if plugin_name in past_list:
|
||||
past_list.remove(plugin_name)
|
||||
new_list_str = self.__getCategoryPluginsConfigFromList(past_list)
|
||||
self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,new_list_str)
|
||||
self.config_has_changed()
|
||||
|
||||
|
||||
|
||||
def registerOptionFromPlugin(self,
|
||||
category_name, plugin_name,
|
||||
option_name, option_value):
|
||||
"""
|
||||
To be called from a plugin object, register a given option in
|
||||
the name of a given plugin.
|
||||
"""
|
||||
section_name = "%s Plugin: %s" % (category_name,plugin_name)
|
||||
# if the plugin's section is not here yet, create it
|
||||
if not self.config_parser.has_section(section_name):
|
||||
self.config_parser.add_section(section_name)
|
||||
# set the required option
|
||||
self.config_parser.set(section_name,option_name,option_value)
|
||||
self.config_has_changed()
|
||||
|
||||
def hasOptionFromPlugin(self,
|
||||
category_name, plugin_name, option_name):
|
||||
"""
|
||||
To be called from a plugin object, return True if the option
|
||||
has already been registered.
|
||||
"""
|
||||
section_name = "%s Plugin: %s" % (category_name,plugin_name)
|
||||
return self.config_parser.has_section(section_name) and self.config_parser.has_option(section_name,option_name)
|
||||
|
||||
def readOptionFromPlugin(self,
|
||||
category_name, plugin_name, option_name):
|
||||
"""
|
||||
To be called from a plugin object, read a given option in
|
||||
the name of a given plugin.
|
||||
"""
|
||||
section_name = "%s Plugin: %s" % (category_name,plugin_name)
|
||||
return self.config_parser.get(section_name,option_name)
|
||||
|
||||
|
||||
def __decoratePluginObject(self, category_name, plugin_name, plugin_object):
|
||||
"""
|
||||
Add two methods to the plugin objects that will make it
|
||||
possible for it to benefit from this class's api concerning
|
||||
the management of the options.
|
||||
"""
|
||||
plugin_object.setConfigOption = lambda x,y: self.registerOptionFromPlugin(category_name,
|
||||
plugin_name,
|
||||
x,y)
|
||||
plugin_object.setConfigOption.__doc__ = self.registerOptionFromPlugin.__doc__
|
||||
plugin_object.getConfigOption = lambda x: self.readOptionFromPlugin(category_name,
|
||||
plugin_name,
|
||||
x)
|
||||
plugin_object.getConfigOption.__doc__ = self.readOptionFromPlugin.__doc__
|
||||
plugin_object.hasConfigOption = lambda x: self.hasOptionFromPlugin(category_name,
|
||||
plugin_name,
|
||||
x)
|
||||
plugin_object.hasConfigOption.__doc__ = self.hasOptionFromPlugin.__doc__
|
||||
|
||||
def activatePluginByName(self, plugin_name, category_name="Default", save_state=True):
|
||||
"""
|
||||
Activate a plugin, , and remember it (in the config file).
|
||||
|
||||
If you want the plugin to benefit from the configuration
|
||||
utility defined by this manager, it is crucial to use this
|
||||
method to activate a plugin and not call the plugin object's
|
||||
``activate`` method. In fact, this method will also "decorate"
|
||||
the plugin object so that it can use this class's methods to
|
||||
register its own options.
|
||||
|
||||
By default, the plugin's activation is registered in the
|
||||
config file but if you d'ont want this set the 'save_state'
|
||||
argument to False.
|
||||
"""
|
||||
# first decorate the plugin
|
||||
pta = self._component.getPluginByName(plugin_name,category_name)
|
||||
if pta is None:
|
||||
return None
|
||||
self.__decoratePluginObject(category_name,plugin_name,pta.plugin_object)
|
||||
# activate the plugin
|
||||
plugin_object = self._component.activatePluginByName(plugin_name,category_name)
|
||||
# check the activation and then optionally set the config option
|
||||
if plugin_object.is_activated:
|
||||
if save_state:
|
||||
self.__addPluginToConfig(category_name,plugin_name)
|
||||
return plugin_object
|
||||
return None
|
||||
|
||||
def deactivatePluginByName(self, plugin_name, category_name="Default", save_state=True):
|
||||
"""
|
||||
Deactivate a plugin, and remember it (in the config file).
|
||||
|
||||
By default, the plugin's deactivation is registered in the
|
||||
config file but if you d'ont want this set the ``save_state``
|
||||
argument to False.
|
||||
"""
|
||||
# activate the plugin
|
||||
plugin_object = self._component.deactivatePluginByName(plugin_name,category_name)
|
||||
if plugin_object is None:
|
||||
return None
|
||||
# check the deactivation and then optionnally set the config option
|
||||
if not plugin_object.is_activated:
|
||||
if save_state:
|
||||
self.__removePluginFromConfig(category_name,plugin_name)
|
||||
return plugin_object
|
||||
return None
|
||||
|
||||
def loadPlugins(self,callback=None, callback_after=None):
|
||||
"""
|
||||
Walk through the plugins' places and look for plugins. Then
|
||||
for each plugin candidate look for its category, load it and
|
||||
stores it in the appropriate slot of the ``category_mapping``.
|
||||
"""
|
||||
self._component.loadPlugins(callback, callback_after)
|
||||
# now load the plugins according to the recorded configuration
|
||||
if self.config_parser.has_section(self.CONFIG_SECTION_NAME):
|
||||
# browse all the categories
|
||||
for category_name in list(self._component.category_mapping.keys()):
|
||||
# get the list of plugins to be activated for this
|
||||
# category
|
||||
option_name = "%s_plugins_to_load"%category_name
|
||||
if self.config_parser.has_option(self.CONFIG_SECTION_NAME,
|
||||
option_name):
|
||||
plugin_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME,
|
||||
option_name)
|
||||
plugin_list = self.__getCategoryPluginsListFromConfig(plugin_list_str)
|
||||
# activate all the plugins that should be
|
||||
# activated
|
||||
for plugin_name in plugin_list:
|
||||
self.activatePluginByName(plugin_name,category_name)
|
||||
|
||||
|
||||
|
||||
|
137
yapsy/FilteredPluginManager.py
Normal file
137
yapsy/FilteredPluginManager.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines the basic mechanisms to have a plugin manager filter the
|
||||
available list of plugins after locating them and before loading them.
|
||||
|
||||
One use fo this would be to prevent untrusted plugins from entering
|
||||
the system.
|
||||
|
||||
To use it properly you must reimplement or monkey patch the
|
||||
``IsPluginOk`` method, as in the following example::
|
||||
|
||||
# define a plugin manager (with you prefered options)
|
||||
pm = PluginManager(...)
|
||||
# decorate it with the Filtering mechanics
|
||||
pm = FilteredPluginManager(pm)
|
||||
# define a custom predicate that filters out plugins without descriptions
|
||||
pm.isPluginOk = lambda x: x.description!=""
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.PluginManagerDecorator import PluginManagerDecorator
|
||||
|
||||
|
||||
class FilteredPluginManager(PluginManagerDecorator):
|
||||
"""
|
||||
Base class for decorators which filter the plugins list
|
||||
before they are loaded.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
decorated_manager=None,
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext="yapsy-plugin"):
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default":IPlugin}
|
||||
# Create the base decorator class
|
||||
PluginManagerDecorator.__init__(self,decorated_manager,
|
||||
categories_filter,
|
||||
directories_list,
|
||||
plugin_info_ext)
|
||||
# prepare the mapping of the latest version of each plugin
|
||||
self.rejectedPlugins = [ ]
|
||||
|
||||
|
||||
|
||||
def filterPlugins(self):
|
||||
"""
|
||||
Go through the currently available candidates, and and either
|
||||
leaves them, or moves them into the list of rejected Plugins.
|
||||
|
||||
Can be overridden if overriding ``isPluginOk`` sentinel is not
|
||||
powerful enough.
|
||||
"""
|
||||
self.rejectedPlugins = [ ]
|
||||
for candidate_infofile, candidate_filepath, plugin_info in self._component.getPluginCandidates():
|
||||
if not self.isPluginOk( plugin_info):
|
||||
self.rejectPluginCandidate((candidate_infofile, candidate_filepath, plugin_info) )
|
||||
|
||||
def rejectPluginCandidate(self,pluginTuple):
|
||||
"""
|
||||
Move a plugin from the candidates list to the rejected List.
|
||||
"""
|
||||
if pluginTuple in self.getPluginCandidates():
|
||||
self._component.removePluginCandidate(pluginTuple)
|
||||
if not pluginTuple in self.rejectedPlugins:
|
||||
self.rejectedPlugins.append(pluginTuple)
|
||||
|
||||
def unrejectPluginCandidate(self,pluginTuple):
|
||||
"""
|
||||
Move a plugin from the rejected list to into the candidates
|
||||
list.
|
||||
"""
|
||||
if not pluginTuple in self.getPluginCandidates():
|
||||
self._component.appendPluginCandidate(pluginTuple)
|
||||
if pluginTuple in self.rejectedPlugins:
|
||||
self.rejectedPlugins.remove(pluginTuple)
|
||||
|
||||
def removePluginCandidate(self,pluginTuple):
|
||||
"""
|
||||
Remove a plugin from the list of candidates.
|
||||
"""
|
||||
if pluginTuple in self.getPluginCandidates():
|
||||
self._component.removePluginCandidate(pluginTuple)
|
||||
if pluginTuple in self.rejectedPlugins:
|
||||
self.rejectedPlugins.remove(pluginTuple)
|
||||
|
||||
|
||||
def appendPluginCandidate(self,pluginTuple):
|
||||
"""
|
||||
Add a new candidate.
|
||||
"""
|
||||
if self.isPluginOk(pluginTuple[2]):
|
||||
if pluginTuple not in self.getPluginCandidates():
|
||||
self._component.appendPluginCandidate(pluginTuple)
|
||||
else:
|
||||
if not pluginTuple in self.rejectedPlugins:
|
||||
self.rejectedPlugins.append(pluginTuple)
|
||||
|
||||
def isPluginOk(self,info):
|
||||
"""
|
||||
Sentinel function to detect if a plugin should be filtered.
|
||||
|
||||
``info`` is an instance of a ``PluginInfo`` and this method is
|
||||
expected to return True if the corresponding plugin can be
|
||||
accepted, and False if it must be filtered out.
|
||||
|
||||
Subclasses should override this function and return false for
|
||||
any plugin which they do not want to be loadable.
|
||||
"""
|
||||
return True
|
||||
|
||||
def locatePlugins(self):
|
||||
"""
|
||||
locate and filter plugins.
|
||||
"""
|
||||
#Reset Catalogue
|
||||
self.setCategoriesFilter(self._component.categories_interfaces)
|
||||
#Reread and filter.
|
||||
self._component.locatePlugins()
|
||||
self.filterPlugins()
|
||||
return len(self._component.getPluginCandidates())
|
||||
|
||||
def getRejectedPlugins(self):
|
||||
"""
|
||||
Return the list of rejected plugins.
|
||||
"""
|
||||
return self.rejectedPlugins[:]
|
46
yapsy/IMultiprocessChildPlugin.py
Normal file
46
yapsy/IMultiprocessChildPlugin.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Originally defined the basic interfaces for multiprocessed plugins.
|
||||
|
||||
Deprecation Note
|
||||
================
|
||||
|
||||
This class is deprecated and replaced by :doc:`IMultiprocessChildPlugin`.
|
||||
|
||||
Child classes of `IMultiprocessChildPlugin` used to be an `IPlugin` as well as
|
||||
a `multiprocessing.Process`, possibly playing with the functionalities of both,
|
||||
which make maintenance harder than necessary.
|
||||
|
||||
And indeed following a bug fix to make multiprocess plugins work on Windows,
|
||||
instances of IMultiprocessChildPlugin inherit Process but are not exactly the
|
||||
running process (there is a new wrapper process).
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
from multiprocessing import Process
|
||||
from yapsy.IMultiprocessPlugin import IMultiprocessPlugin
|
||||
|
||||
|
||||
class IMultiprocessChildPlugin(IMultiprocessPlugin, Process):
|
||||
"""
|
||||
Base class for multiprocessed plugin.
|
||||
|
||||
DEPRECATED(>1.11): Please use IMultiProcessPluginBase instead !
|
||||
"""
|
||||
|
||||
def __init__(self, parent_pipe):
|
||||
IMultiprocessPlugin.__init__(self, parent_pipe)
|
||||
Process.__init__(self)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Override this method in your implementation
|
||||
"""
|
||||
return
|
47
yapsy/IMultiprocessPlugin.py
Normal file
47
yapsy/IMultiprocessPlugin.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines the basic interfaces for multiprocessed plugins.
|
||||
|
||||
Extensibility
|
||||
=============
|
||||
|
||||
In your own software, you'll probably want to build derived classes of
|
||||
the ``IMultiprocessPlugin`` class as it is a mere interface with no specific
|
||||
functionality.
|
||||
|
||||
Your software's plugins should then inherit your very own plugin class
|
||||
(itself derived from ``IMultiprocessPlugin``).
|
||||
|
||||
Override the `run` method to include your code. Use the `self.parent_pipe` to send
|
||||
and receive data with the parent process or create your own communication
|
||||
mecanism.
|
||||
|
||||
Where and how to code these plugins is explained in the section about
|
||||
the :doc:`PluginManager`.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
|
||||
class IMultiprocessPlugin(IPlugin):
|
||||
"""
|
||||
Base class for multiprocessed plugin.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_pipe):
|
||||
IPlugin.__init__(self)
|
||||
self.parent_pipe = parent_pipe
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Override this method in your implementation
|
||||
"""
|
||||
return
|
56
yapsy/IPlugin.py
Normal file
56
yapsy/IPlugin.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines the basic interfaces for a plugin. These interfaces are
|
||||
inherited by the *core* class of a plugin. The *core* class of a
|
||||
plugin is then the one that will be notified the
|
||||
activation/deactivation of a plugin via the ``activate/deactivate``
|
||||
methods.
|
||||
|
||||
|
||||
For simple (near trivial) plugin systems, one can directly use the
|
||||
following interfaces.
|
||||
|
||||
Extensibility
|
||||
=============
|
||||
|
||||
In your own software, you'll probably want to build derived classes of
|
||||
the ``IPlugin`` class as it is a mere interface with no specific
|
||||
functionality.
|
||||
|
||||
Your software's plugins should then inherit your very own plugin class
|
||||
(itself derived from ``IPlugin``).
|
||||
|
||||
Where and how to code these plugins is explained in the section about
|
||||
the :doc:`PluginManager`.
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
|
||||
class IPlugin(object):
|
||||
"""
|
||||
The most simple interface to be inherited when creating a plugin.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_activated = False
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
Called at plugin activation.
|
||||
"""
|
||||
self.is_activated = True
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
Called when the plugin is disabled.
|
||||
"""
|
||||
self.is_activated = False
|
||||
|
104
yapsy/IPluginLocator.py
Normal file
104
yapsy/IPluginLocator.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
``IPluginLocator`` defines the basic interface expected by a
|
||||
``PluginManager`` to be able to locate plugins and get basic info
|
||||
about each discovered plugin (name, version etc).
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from yapsy import log
|
||||
|
||||
class IPluginLocator(object):
|
||||
"""
|
||||
Plugin Locator interface with some methods already implemented to
|
||||
manage the awkward backward compatible stuff.
|
||||
"""
|
||||
|
||||
def locatePlugins(self):
|
||||
"""
|
||||
Walk through the plugins' places and look for plugins.
|
||||
|
||||
Return the discovered plugins as a list of
|
||||
``(candidate_infofile_path, candidate_file_path,plugin_info_instance)``
|
||||
and their number.
|
||||
"""
|
||||
raise NotImplementedError("locatePlugins must be reimplemented by %s" % self)
|
||||
|
||||
def gatherCorePluginInfo(self, directory, filename):
|
||||
"""
|
||||
Return a ``PluginInfo`` as well as the ``ConfigParser`` used to build it.
|
||||
|
||||
If filename is a valid plugin discovered by any of the known
|
||||
strategy in use. Returns None,None otherwise.
|
||||
"""
|
||||
raise NotImplementedError("gatherPluginInfo must be reimplemented by %s" % self)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Below are backward compatibility methods: if you inherit from
|
||||
# IPluginLocator it's ok not to reimplement them, there will only
|
||||
# be a warning message logged if they are called and not
|
||||
# reimplemented.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def getPluginNameAndModuleFromStream(self,fileobj):
|
||||
"""
|
||||
DEPRECATED(>1.9): kept for backward compatibility
|
||||
with existing PluginManager child classes.
|
||||
|
||||
Return a 3-uple with the name of the plugin, its
|
||||
module and the config_parser used to gather the core
|
||||
data *in a tuple*, if the required info could be
|
||||
localised, else return ``(None,None,None)``.
|
||||
"""
|
||||
log.warning("setPluginInfoClass was called but '%s' doesn't implement it." % self)
|
||||
return None,None,None
|
||||
|
||||
|
||||
def setPluginInfoClass(self, picls, names=None):
|
||||
"""
|
||||
DEPRECATED(>1.9): kept for backward compatibility
|
||||
with existing PluginManager child classes.
|
||||
|
||||
Set the class that holds PluginInfo. The class should inherit
|
||||
from ``PluginInfo``.
|
||||
"""
|
||||
log.warning("setPluginInfoClass was called but '%s' doesn't implement it." % self)
|
||||
|
||||
def getPluginInfoClass(self):
|
||||
"""
|
||||
DEPRECATED(>1.9): kept for backward compatibility
|
||||
with existing PluginManager child classes.
|
||||
|
||||
Get the class that holds PluginInfo.
|
||||
"""
|
||||
log.warning("getPluginInfoClass was called but '%s' doesn't implement it." % self)
|
||||
return None
|
||||
|
||||
def setPluginPlaces(self, directories_list):
|
||||
"""
|
||||
DEPRECATED(>1.9): kept for backward compatibility
|
||||
with existing PluginManager child classes.
|
||||
|
||||
Set the list of directories where to look for plugin places.
|
||||
"""
|
||||
log.warning("setPluginPlaces was called but '%s' doesn't implement it." % self)
|
||||
|
||||
def updatePluginPlaces(self, directories_list):
|
||||
"""
|
||||
DEPRECATED(>1.9): kept for backward compatibility
|
||||
with existing PluginManager child classes.
|
||||
|
||||
Updates the list of directories where to look for plugin places.
|
||||
"""
|
||||
log.warning("updatePluginPlaces was called but '%s' doesn't implement it." % self)
|
||||
|
96
yapsy/MultiprocessPluginManager.py
Normal file
96
yapsy/MultiprocessPluginManager.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines a plugin manager that runs all plugins in separate process
|
||||
linked by pipes.
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
import multiprocessing as mproc
|
||||
|
||||
from yapsy.IMultiprocessPlugin import IMultiprocessPlugin
|
||||
from yapsy.IMultiprocessChildPlugin import IMultiprocessChildPlugin
|
||||
from yapsy.MultiprocessPluginProxy import MultiprocessPluginProxy
|
||||
from yapsy.PluginManager import PluginManager
|
||||
|
||||
|
||||
class MultiprocessPluginManager(PluginManager):
|
||||
"""
|
||||
Subclass of the PluginManager that runs each plugin in a different process
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext=None,
|
||||
plugin_locator=None):
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default": IMultiprocessPlugin}
|
||||
PluginManager.__init__(self,
|
||||
categories_filter=categories_filter,
|
||||
directories_list=directories_list,
|
||||
plugin_info_ext=plugin_info_ext,
|
||||
plugin_locator=plugin_locator)
|
||||
self.connections = []
|
||||
|
||||
|
||||
def instanciateElementWithImportInfo(self, element, element_name,
|
||||
plugin_module_name, candidate_filepath):
|
||||
"""This method instanciates each plugin in a new process and links it to
|
||||
the parent with a pipe.
|
||||
|
||||
In the parent process context, the plugin's class is replaced by
|
||||
the ``MultiprocessPluginProxy`` class that hold the information
|
||||
about the child process and the pipe to communicate with it.
|
||||
|
||||
.. warning::
|
||||
The plugin code should only use the pipe to
|
||||
communicate with the rest of the applica`tion and should not
|
||||
assume any kind of shared memory, not any specific functionality
|
||||
of the `multiprocessing.Process` parent class (its behaviour is
|
||||
different between platforms !)
|
||||
|
||||
See ``IMultiprocessPlugin``.
|
||||
"""
|
||||
if element is IMultiprocessChildPlugin:
|
||||
# The following will keep retro compatibility for IMultiprocessChildPlugin
|
||||
raise Exception("Preventing instanciation of a bar child plugin interface.")
|
||||
instanciated_element = MultiprocessPluginProxy()
|
||||
parent_pipe, child_pipe = mproc.Pipe()
|
||||
instanciated_element.child_pipe = parent_pipe
|
||||
instanciated_element.proc = MultiprocessPluginManager._PluginProcessWrapper(
|
||||
element_name, plugin_module_name, candidate_filepath,
|
||||
child_pipe)
|
||||
instanciated_element.proc.start()
|
||||
return instanciated_element
|
||||
|
||||
|
||||
class _PluginProcessWrapper(mproc.Process):
|
||||
"""Helper class that strictly needed to be able to spawn the
|
||||
plugin on Windows but kept also for Unix platform to get a more
|
||||
uniform behaviour.
|
||||
|
||||
This will handle re-importing the plugin's module in the child
|
||||
process (again this is necessary on windows because what has
|
||||
been imported in the main thread/process will not be shared with
|
||||
the spawned process.)
|
||||
"""
|
||||
def __init__(self, element_name, plugin_module_name, candidate_filepath, child_pipe):
|
||||
self.element_name = element_name
|
||||
self.child_pipe = child_pipe
|
||||
self.plugin_module_name = plugin_module_name
|
||||
self.candidate_filepath = candidate_filepath
|
||||
mproc.Process.__init__(self)
|
||||
|
||||
def run(self):
|
||||
module = PluginManager._importModule(self.plugin_module_name,
|
||||
self.candidate_filepath)
|
||||
element = getattr(module, self.element_name)
|
||||
e = element(self.child_pipe)
|
||||
e.run()
|
33
yapsy/MultiprocessPluginProxy.py
Normal file
33
yapsy/MultiprocessPluginProxy.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
The ``MultiprocessPluginProxy`` is instanciated by the MultiprocessPluginManager to replace the real implementation
|
||||
that is run in a different process.
|
||||
|
||||
You cannot access your plugin directly from the parent process. You should use the child_pipe to communicate
|
||||
with your plugin. The `MultiprocessPluginProxy`` role is to keep reference of the communication pipe to the
|
||||
child process as well as the process informations.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
|
||||
|
||||
class MultiprocessPluginProxy(IPlugin):
|
||||
"""
|
||||
This class contains two members that are initialized by the :doc:`MultiprocessPluginManager`.
|
||||
|
||||
self.proc is a reference that holds the multiprocessing.Process instance of the child process.
|
||||
|
||||
self.child_pipe is a reference that holds the multiprocessing.Pipe instance to communicate with the child.
|
||||
"""
|
||||
def __init__(self):
|
||||
IPlugin.__init__(self)
|
||||
self.proc = None # This attribute holds the multiprocessing.Process instance
|
||||
self.child_pipe = None # This attribute holds the multiprocessing.Pipe instance
|
539
yapsy/PluginFileLocator.py
Normal file
539
yapsy/PluginFileLocator.py
Normal file
@@ -0,0 +1,539 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
The ``PluginFileLocator`` locates plugins when they are accessible via the filesystem.
|
||||
|
||||
It's default behaviour is to look for text files with the
|
||||
'.yapsy-plugin' extensions and to read the plugin's decription in
|
||||
them.
|
||||
|
||||
|
||||
Customization
|
||||
-------------
|
||||
|
||||
The behaviour of a ``PluginFileLocator`` can be customized by instanciating it with a specific 'analyzer'.
|
||||
|
||||
Two analyzers are already implemented and provided here:
|
||||
|
||||
``PluginFileAnalyzerWithInfoFile``
|
||||
|
||||
the default 'analyzer' that looks for plugin 'info files' as
|
||||
text file with a predefined extension. This implements the way
|
||||
yapsy looks for plugin since version 1.
|
||||
|
||||
``PluginFileAnalyzerMathingRegex``
|
||||
|
||||
look for files matching a regex and considers them as being
|
||||
the plugin itself.
|
||||
|
||||
All analyzers must enforce the
|
||||
|
||||
It enforces the ``plugin locator`` policy as defined by ``IPluginLocator`` and used by ``PluginManager``.
|
||||
|
||||
``info_ext``
|
||||
|
||||
expects a plugin to be discovered through its *plugin info file*.
|
||||
User just needs to provide an extension (without '.') to look
|
||||
for *plugin_info_file*.
|
||||
|
||||
``regexp``
|
||||
|
||||
looks for file matching the given regular pattern expression.
|
||||
User just needs to provide the regular pattern expression.
|
||||
|
||||
All analyzers must enforce the policy represented by the ``IPluginFileAnalyzer`` interface.
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from yapsy import log
|
||||
from yapsy.compat import ConfigParser, is_py2, basestring
|
||||
|
||||
from yapsy.PluginInfo import PluginInfo
|
||||
from yapsy import PLUGIN_NAME_FORBIDEN_STRING
|
||||
from yapsy.IPluginLocator import IPluginLocator
|
||||
|
||||
|
||||
|
||||
|
||||
class IPluginFileAnalyzer(object):
|
||||
"""
|
||||
Define the methods expected by PluginFileLocator for its 'analyzer'.
|
||||
"""
|
||||
|
||||
def __init__(self,name):
|
||||
self.name = name
|
||||
|
||||
def isValidPlugin(self, filename):
|
||||
"""
|
||||
Check if the resource found at filename is a valid plugin.
|
||||
"""
|
||||
raise NotImplementedError("'isValidPlugin' must be reimplemented by %s" % self)
|
||||
|
||||
|
||||
def getInfosDictFromPlugin(self, dirpath, filename):
|
||||
"""
|
||||
Returns the extracted plugin informations as a dictionary.
|
||||
This function ensures that "name" and "path" are provided.
|
||||
|
||||
*dirpath* is the full path to the directory where the plugin file is
|
||||
|
||||
*filename* is the name (ie the basename) of the plugin file.
|
||||
|
||||
If *callback* function has not been provided for this strategy,
|
||||
we use the filename alone to extract minimal informations.
|
||||
"""
|
||||
raise NotImplementedError("'getInfosDictFromPlugin' must be reimplemented by %s" % self)
|
||||
|
||||
|
||||
class PluginFileAnalyzerWithInfoFile(IPluginFileAnalyzer):
|
||||
"""
|
||||
Consider plugins described by a textual description file.
|
||||
|
||||
A plugin is expected to be described by a text file ('ini' format) with a specific extension (.yapsy-plugin by default).
|
||||
|
||||
This file must contain at least the following information::
|
||||
|
||||
[Core]
|
||||
Name = name of the module
|
||||
Module = relative_path/to/python_file_or_directory
|
||||
|
||||
Optionnally the description file may also contain the following section (in addition to the above one)::
|
||||
|
||||
[Documentation]
|
||||
Author = Author Name
|
||||
Version = Major.minor
|
||||
Website = url_for_plugin
|
||||
Description = A simple one-sentence description
|
||||
|
||||
Ctor Arguments:
|
||||
|
||||
*name* name of the analyzer.
|
||||
|
||||
*extensions* the expected extensions for the plugin info file. May be a string or a tuple of strings if several extensions are expected.
|
||||
"""
|
||||
|
||||
def __init__(self, name, extensions="yapsy-plugin"):
|
||||
IPluginFileAnalyzer.__init__(self,name)
|
||||
self.setPluginInfoExtension(extensions)
|
||||
|
||||
|
||||
def setPluginInfoExtension(self,extensions):
|
||||
"""
|
||||
Set the extension that will identify a plugin info file.
|
||||
|
||||
*extensions* May be a string or a tuple of strings if several extensions are expected.
|
||||
"""
|
||||
# Make sure extension is a tuple
|
||||
if not isinstance(extensions, tuple):
|
||||
extensions = (extensions, )
|
||||
self.expectedExtensions = extensions
|
||||
|
||||
|
||||
def isValidPlugin(self, filename):
|
||||
"""
|
||||
Check if it is a valid plugin based on the given plugin info file extension(s).
|
||||
If several extensions are provided, the first matching will cause the function
|
||||
to exit successfully.
|
||||
"""
|
||||
res = False
|
||||
for ext in self.expectedExtensions:
|
||||
if filename.endswith(".%s" % ext):
|
||||
res = True
|
||||
break
|
||||
return res
|
||||
|
||||
def getPluginNameAndModuleFromStream(self, infoFileObject, candidate_infofile=None):
|
||||
"""
|
||||
Extract the name and module of a plugin from the
|
||||
content of the info file that describes it and which
|
||||
is stored in ``infoFileObject``.
|
||||
|
||||
.. note:: Prefer using ``_extractCorePluginInfo``
|
||||
instead, whenever possible...
|
||||
|
||||
.. warning:: ``infoFileObject`` must be a file-like object:
|
||||
either an opened file for instance or a string
|
||||
buffer wrapped in a StringIO instance as another
|
||||
example.
|
||||
|
||||
.. note:: ``candidate_infofile`` must be provided
|
||||
whenever possible to get better error messages.
|
||||
|
||||
Return a 3-uple with the name of the plugin, its
|
||||
module and the config_parser used to gather the core
|
||||
data *in a tuple*, if the required info could be
|
||||
localised, else return ``(None,None,None)``.
|
||||
|
||||
.. note:: This is supposed to be used internally by subclasses
|
||||
and decorators.
|
||||
"""
|
||||
# parse the information buffer to get info about the plugin
|
||||
config_parser = ConfigParser()
|
||||
try:
|
||||
if is_py2:
|
||||
config_parser.readfp(infoFileObject)
|
||||
else:
|
||||
config_parser.read_file(infoFileObject)
|
||||
except Exception as e:
|
||||
log.debug("Could not parse the plugin file '%s' (exception raised was '%s')" % (candidate_infofile,e))
|
||||
return (None, None, None)
|
||||
# check if the basic info is available
|
||||
if not config_parser.has_section("Core"):
|
||||
log.debug("Plugin info file has no 'Core' section (in '%s')" % candidate_infofile)
|
||||
return (None, None, None)
|
||||
if not config_parser.has_option("Core","Name") or not config_parser.has_option("Core","Module"):
|
||||
log.debug("Plugin info file has no 'Name' or 'Module' section (in '%s')" % candidate_infofile)
|
||||
return (None, None, None)
|
||||
# check that the given name is valid
|
||||
name = config_parser.get("Core", "Name")
|
||||
name = name.strip()
|
||||
if PLUGIN_NAME_FORBIDEN_STRING in name:
|
||||
log.debug("Plugin name contains forbiden character: %s (in '%s')" % (PLUGIN_NAME_FORBIDEN_STRING,
|
||||
candidate_infofile))
|
||||
return (None, None, None)
|
||||
return (name, config_parser.get("Core", "Module"), config_parser)
|
||||
|
||||
def _extractCorePluginInfo(self,directory, filename):
|
||||
"""
|
||||
Gather the core information (name, and module to be loaded)
|
||||
about a plugin described by it's info file (found at
|
||||
'directory/filename').
|
||||
|
||||
Return a dictionary with name and path of the plugin as well
|
||||
as the ConfigParser instance used to collect these info.
|
||||
|
||||
.. note:: This is supposed to be used internally by subclasses
|
||||
and decorators.
|
||||
"""
|
||||
# now we can consider the file as a serious candidate
|
||||
if not isinstance(filename, basestring):
|
||||
# filename is a file object: use it
|
||||
name, moduleName, config_parser = self.getPluginNameAndModuleFromStream(filename)
|
||||
else:
|
||||
candidate_infofile_path = os.path.join(directory, filename)
|
||||
# parse the information file to get info about the plugin
|
||||
with open(candidate_infofile_path) as candidate_infofile:
|
||||
name, moduleName, config_parser = self.getPluginNameAndModuleFromStream(candidate_infofile,candidate_infofile_path)
|
||||
if (name, moduleName, config_parser) == (None, None, None):
|
||||
return (None,None)
|
||||
infos = {"name":name, "path":os.path.join(directory, moduleName)}
|
||||
return infos, config_parser
|
||||
|
||||
def _extractBasicPluginInfo(self,directory, filename):
|
||||
"""
|
||||
Gather some basic documentation about the plugin described by
|
||||
it's info file (found at 'directory/filename').
|
||||
|
||||
Return a dictionary containing the core information (name and
|
||||
path) as well as as the 'documentation' info (version, author,
|
||||
description etc).
|
||||
|
||||
See also:
|
||||
|
||||
``self._extractCorePluginInfo``
|
||||
"""
|
||||
infos, config_parser = self._extractCorePluginInfo(directory, filename)
|
||||
# collect additional (but usually quite usefull) information
|
||||
if infos and config_parser and config_parser.has_section("Documentation"):
|
||||
if config_parser.has_option("Documentation","Author"):
|
||||
infos["author"] = config_parser.get("Documentation", "Author")
|
||||
if config_parser.has_option("Documentation","Version"):
|
||||
infos["version"] = config_parser.get("Documentation", "Version")
|
||||
if config_parser.has_option("Documentation","Website"):
|
||||
infos["website"] = config_parser.get("Documentation", "Website")
|
||||
if config_parser.has_option("Documentation","Copyright"):
|
||||
infos["copyright"] = config_parser.get("Documentation", "Copyright")
|
||||
if config_parser.has_option("Documentation","Description"):
|
||||
infos["description"] = config_parser.get("Documentation", "Description")
|
||||
return infos, config_parser
|
||||
|
||||
def getInfosDictFromPlugin(self, dirpath, filename):
|
||||
"""
|
||||
Returns the extracted plugin informations as a dictionary.
|
||||
This function ensures that "name" and "path" are provided.
|
||||
|
||||
If *callback* function has not been provided for this strategy,
|
||||
we use the filename alone to extract minimal informations.
|
||||
"""
|
||||
infos, config_parser = self._extractBasicPluginInfo(dirpath, filename)
|
||||
if not infos or infos.get("name", None) is None:
|
||||
raise ValueError("Missing *name* of the plugin in extracted infos.")
|
||||
if not infos or infos.get("path", None) is None:
|
||||
raise ValueError("Missing *path* of the plugin in extracted infos.")
|
||||
return infos, config_parser
|
||||
|
||||
|
||||
class PluginFileAnalyzerMathingRegex(IPluginFileAnalyzer):
|
||||
"""
|
||||
An analyzer that targets plugins decribed by files whose name match a given regex.
|
||||
"""
|
||||
def __init__(self, name, regexp):
|
||||
IPluginFileAnalyzer.__init__(self,name)
|
||||
self.regexp = regexp
|
||||
|
||||
def isValidPlugin(self, filename):
|
||||
"""
|
||||
Checks if the given filename is a valid plugin for this Strategy
|
||||
"""
|
||||
reg = re.compile(self.regexp)
|
||||
if reg.match(filename) is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getInfosDictFromPlugin(self, dirpath, filename):
|
||||
"""
|
||||
Returns the extracted plugin informations as a dictionary.
|
||||
This function ensures that "name" and "path" are provided.
|
||||
"""
|
||||
# use the filename alone to extract minimal informations.
|
||||
infos = {}
|
||||
module_name = os.path.splitext(filename)[0]
|
||||
plugin_filename = os.path.join(dirpath,filename)
|
||||
if module_name == "__init__":
|
||||
module_name = os.path.basename(dirpath)
|
||||
plugin_filename = dirpath
|
||||
infos["name"] = "%s" % module_name
|
||||
infos["path"] = plugin_filename
|
||||
cf_parser = ConfigParser()
|
||||
cf_parser.add_section("Core")
|
||||
cf_parser.set("Core","Name",infos["name"])
|
||||
cf_parser.set("Core","Module",infos["path"])
|
||||
return infos,cf_parser
|
||||
|
||||
|
||||
|
||||
class PluginFileLocator(IPluginLocator):
|
||||
"""
|
||||
Locates plugins on the file system using a set of analyzers to
|
||||
determine what files actually corresponds to plugins.
|
||||
|
||||
If more than one analyzer is being used, the first that will discover a
|
||||
new plugin will avoid other strategies to find it too.
|
||||
|
||||
By default each directory set as a "plugin place" is scanned
|
||||
recursively. You can change that by a call to
|
||||
``disableRecursiveScan``.
|
||||
"""
|
||||
|
||||
def __init__(self, analyzers=None, plugin_info_cls=PluginInfo):
|
||||
IPluginLocator.__init__(self)
|
||||
self._discovered_plugins = {}
|
||||
self.setPluginPlaces(None)
|
||||
self._analyzers = analyzers # analyzers used to locate plugins
|
||||
if self._analyzers is None:
|
||||
self._analyzers = [PluginFileAnalyzerWithInfoFile("info_ext")]
|
||||
self._default_plugin_info_cls = plugin_info_cls
|
||||
self._plugin_info_cls_map = {}
|
||||
self._max_size = 1e3*1024 # in octets (by default 1 Mo)
|
||||
self.recursive = True
|
||||
|
||||
def disableRecursiveScan(self):
|
||||
"""
|
||||
Disable recursive scan of the directories given as plugin places.
|
||||
"""
|
||||
self.recursive = False
|
||||
|
||||
def setAnalyzers(self, analyzers):
|
||||
"""
|
||||
Sets a new set of analyzers.
|
||||
|
||||
.. warning:: the new analyzers won't be aware of the plugin
|
||||
info class that may have been set via a previous
|
||||
call to ``setPluginInfoClass``.
|
||||
"""
|
||||
self._analyzers = analyzers
|
||||
|
||||
def removeAnalyzers(self, name):
|
||||
"""
|
||||
Removes analyzers of a given name.
|
||||
"""
|
||||
analyzersListCopy = self._analyzers[:]
|
||||
foundAndRemoved = False
|
||||
for obj in analyzersListCopy:
|
||||
if obj.name == name:
|
||||
self._analyzers.remove(obj)
|
||||
foundAndRemoved = True
|
||||
if not foundAndRemoved:
|
||||
log.debug("'%s' is not a known strategy name: can't remove it." % name)
|
||||
|
||||
def removeAllAnalyzer(self):
|
||||
"""
|
||||
Remove all analyzers.
|
||||
"""
|
||||
self._analyzers = []
|
||||
|
||||
def appendAnalyzer(self, analyzer):
|
||||
"""
|
||||
Append an analyzer to the existing list.
|
||||
"""
|
||||
self._analyzers.append(analyzer)
|
||||
|
||||
|
||||
def _getInfoForPluginFromAnalyzer(self,analyzer,dirpath, filename):
|
||||
"""
|
||||
Return an instance of plugin_info_cls filled with data extracted by the analyzer.
|
||||
|
||||
May return None if the analyzer fails to extract any info.
|
||||
"""
|
||||
plugin_info_dict,config_parser = analyzer.getInfosDictFromPlugin(dirpath, filename)
|
||||
if plugin_info_dict is None:
|
||||
return None
|
||||
plugin_info_cls = self._plugin_info_cls_map.get(analyzer.name,self._default_plugin_info_cls)
|
||||
plugin_info = plugin_info_cls(plugin_info_dict["name"],plugin_info_dict["path"])
|
||||
plugin_info.details = config_parser
|
||||
return plugin_info
|
||||
|
||||
def locatePlugins(self):
|
||||
"""
|
||||
Walk through the plugins' places and look for plugins.
|
||||
|
||||
Return the candidates and number of plugins found.
|
||||
"""
|
||||
# print "%s.locatePlugins" % self.__class__
|
||||
_candidates = []
|
||||
_discovered = {}
|
||||
for directory in map(os.path.abspath, self.plugins_places):
|
||||
# first of all, is it a directory :)
|
||||
if not os.path.isdir(directory):
|
||||
log.debug("%s skips %s (not a directory)" % (self.__class__.__name__, directory))
|
||||
continue
|
||||
if self.recursive:
|
||||
debug_txt_mode = "recursively"
|
||||
walk_iter = os.walk(directory, followlinks=True)
|
||||
else:
|
||||
debug_txt_mode = "non-recursively"
|
||||
walk_iter = [(directory,[],os.listdir(directory))]
|
||||
# iteratively walks through the directory
|
||||
log.debug("%s walks (%s) into directory: %s" % (self.__class__.__name__, debug_txt_mode, directory))
|
||||
for item in walk_iter:
|
||||
dirpath = item[0]
|
||||
for filename in item[2]:
|
||||
# print("testing candidate file %s" % filename)
|
||||
for analyzer in self._analyzers:
|
||||
# print("... with analyzer %s" % analyzer.name)
|
||||
# eliminate the obvious non plugin files
|
||||
if not analyzer.isValidPlugin(filename):
|
||||
log.debug("%s is not a valid plugin for strategy %s" % (filename, analyzer.name))
|
||||
continue
|
||||
candidate_infofile = os.path.join(dirpath, filename)
|
||||
if candidate_infofile in _discovered:
|
||||
log.debug("%s (with strategy %s) rejected because already discovered" % (candidate_infofile, analyzer.name))
|
||||
continue
|
||||
log.debug("%s found a candidate:\n %s" % (self.__class__.__name__, candidate_infofile))
|
||||
# print candidate_infofile
|
||||
plugin_info = self._getInfoForPluginFromAnalyzer(analyzer, dirpath, filename)
|
||||
if plugin_info is None:
|
||||
log.debug("Plugin candidate '%s' rejected by strategy '%s'" % (candidate_infofile, analyzer.name))
|
||||
break # we consider this was the good strategy to use for: it failed -> not a plugin -> don't try another strategy
|
||||
# now determine the path of the file to execute,
|
||||
# depending on wether the path indicated is a
|
||||
# directory or a file
|
||||
# print plugin_info.path
|
||||
# Remember all the files belonging to a discovered
|
||||
# plugin, so that strategies (if several in use) won't
|
||||
# collide
|
||||
if os.path.isdir(plugin_info.path):
|
||||
candidate_filepath = os.path.join(plugin_info.path, "__init__")
|
||||
# it is a package, adds all the files concerned
|
||||
for _file in os.listdir(plugin_info.path):
|
||||
if _file.endswith(".py"):
|
||||
self._discovered_plugins[os.path.join(plugin_info.path, _file)] = candidate_filepath
|
||||
_discovered[os.path.join(plugin_info.path, _file)] = candidate_filepath
|
||||
elif (plugin_info.path.endswith(".py") and os.path.isfile(plugin_info.path)) or os.path.isfile(plugin_info.path+".py"):
|
||||
candidate_filepath = plugin_info.path
|
||||
if candidate_filepath.endswith(".py"):
|
||||
candidate_filepath = candidate_filepath[:-3]
|
||||
# it is a file, adds it
|
||||
self._discovered_plugins[".".join((plugin_info.path, "py"))] = candidate_filepath
|
||||
_discovered[".".join((plugin_info.path, "py"))] = candidate_filepath
|
||||
else:
|
||||
log.error("Plugin candidate rejected: cannot find the file or directory module for '%s'" % (candidate_infofile))
|
||||
break
|
||||
# print candidate_filepath
|
||||
_candidates.append((candidate_infofile, candidate_filepath, plugin_info))
|
||||
# finally the candidate_infofile must not be discovered again
|
||||
_discovered[candidate_infofile] = candidate_filepath
|
||||
self._discovered_plugins[candidate_infofile] = candidate_filepath
|
||||
# print "%s found by strategy %s" % (candidate_filepath, analyzer.name)
|
||||
return _candidates, len(_candidates)
|
||||
|
||||
def gatherCorePluginInfo(self, directory, filename):
|
||||
"""
|
||||
Return a ``PluginInfo`` as well as the ``ConfigParser`` used to build it.
|
||||
|
||||
If filename is a valid plugin discovered by any of the known
|
||||
strategy in use. Returns None,None otherwise.
|
||||
"""
|
||||
for analyzer in self._analyzers:
|
||||
# eliminate the obvious non plugin files
|
||||
if not analyzer.isValidPlugin(filename):
|
||||
continue
|
||||
plugin_info = self._getInfoForPluginFromAnalyzer(analyzer,directory, filename)
|
||||
return plugin_info,plugin_info.details
|
||||
return None,None
|
||||
|
||||
# -----------------------------------------------
|
||||
# Backward compatible methods
|
||||
# Note: their implementation must be conform to their
|
||||
# counterpart in yapsy<1.10
|
||||
# -----------------------------------------------
|
||||
|
||||
def getPluginNameAndModuleFromStream(self, infoFileObject, candidate_infofile=None):
|
||||
for analyzer in self._analyzers:
|
||||
if analyzer.name == "info_ext":
|
||||
return analyzer.getPluginNameAndModuleFromStream(infoFileObject)
|
||||
else:
|
||||
raise RuntimeError("No current file analyzer is able to provide plugin information from stream")
|
||||
|
||||
def setPluginInfoClass(self, picls, name=None):
|
||||
"""
|
||||
Set the class that holds PluginInfo. The class should inherit
|
||||
from ``PluginInfo``.
|
||||
|
||||
If name is given, then the class will be used only by the corresponding analyzer.
|
||||
|
||||
If name is None, the class will be set for all analyzers.
|
||||
"""
|
||||
if name is None:
|
||||
self._default_plugin_info_cls = picls
|
||||
self._plugin_info_cls_map = {}
|
||||
else:
|
||||
self._plugin_info_cls_map[name] = picls
|
||||
|
||||
def setPluginPlaces(self, directories_list):
|
||||
"""
|
||||
Set the list of directories where to look for plugin places.
|
||||
"""
|
||||
if isinstance(directories_list, basestring):
|
||||
raise ValueError("'directories_list' given as a string, but expected to be a list or enumeration of strings")
|
||||
if directories_list is None:
|
||||
directories_list = [os.path.dirname(__file__)]
|
||||
self.plugins_places = directories_list
|
||||
|
||||
def updatePluginPlaces(self, directories_list):
|
||||
"""
|
||||
Updates the list of directories where to look for plugin places.
|
||||
"""
|
||||
self.plugins_places = list(set.union(set(directories_list), set(self.plugins_places)))
|
||||
|
||||
def setPluginInfoExtension(self, ext):
|
||||
"""
|
||||
DEPRECATED(>1.9): for backward compatibility. Directly configure the
|
||||
IPluginLocator instance instead !
|
||||
|
||||
This will only work if the strategy "info_ext" is active
|
||||
for locating plugins.
|
||||
"""
|
||||
for analyzer in self._analyzers:
|
||||
if analyzer.name == "info_ext":
|
||||
analyzer.setPluginInfoExtension(ext)
|
213
yapsy/PluginInfo.py
Normal file
213
yapsy/PluginInfo.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Encapsulate a plugin instance as well as some metadata.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
from yapsy.compat import ConfigParser
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
|
||||
class PluginInfo(object):
|
||||
"""Representation of the most basic set of information related to a
|
||||
given plugin such as its name, author, description...
|
||||
|
||||
Any additional information can be stored ad retrieved in a
|
||||
PluginInfo, when this one is created with a
|
||||
``ConfigParser.ConfigParser`` instance.
|
||||
|
||||
This typically means that when metadata is read from a text file
|
||||
(the original way for yapsy to describe plugins), all info that is
|
||||
not part of the basic variables (name, path, version etc), can
|
||||
still be accessed though the ``details`` member variables that
|
||||
behaves like Python's ``ConfigParser.ConfigParser``.
|
||||
|
||||
.. warning::
|
||||
The instance associated with the ``details`` member
|
||||
variable is never copied and used to store all plugin infos. If
|
||||
you set it to a custom instance, it will be modified as soon as
|
||||
another member variale of the plugin info is
|
||||
changed. Alternatively, if you change the instance "outside" the
|
||||
plugin info, it will also change the plugin info.
|
||||
|
||||
Ctor Arguments:
|
||||
|
||||
:plugin_name: is a simple string describing the name of
|
||||
the plugin.
|
||||
|
||||
:plugin_path: describe the location where the plugin can be
|
||||
found.
|
||||
|
||||
.. warning::
|
||||
The ``path`` attribute is the full path to the
|
||||
plugin if it is organised as a directory or the
|
||||
full path to a file without the ``.py`` extension
|
||||
if the plugin is defined by a simple file. In the
|
||||
later case, the actual plugin is reached via
|
||||
``plugin_info.path+'.py'``.
|
||||
"""
|
||||
|
||||
def __init__(self, plugin_name, plugin_path):
|
||||
self.__details = ConfigParser()
|
||||
self.name = plugin_name
|
||||
self.path = plugin_path
|
||||
self._ensureDetailsDefaultsAreBackwardCompatible()
|
||||
# Storage for stuff created during the plugin lifetime
|
||||
self.plugin_object = None
|
||||
self.categories = []
|
||||
self.error = None
|
||||
|
||||
|
||||
def __setDetails(self,cfDetails):
|
||||
"""
|
||||
Fill in all details by storing a ``ConfigParser`` instance.
|
||||
|
||||
.. warning::
|
||||
The values for ``plugin_name`` and
|
||||
``plugin_path`` given a init time will superseed
|
||||
any value found in ``cfDetails`` in section
|
||||
'Core' for the options 'Name' and 'Module' (this
|
||||
is mostly for backward compatibility).
|
||||
"""
|
||||
bkp_name = self.name
|
||||
bkp_path = self.path
|
||||
self.__details = cfDetails
|
||||
self.name = bkp_name
|
||||
self.path = bkp_path
|
||||
self._ensureDetailsDefaultsAreBackwardCompatible()
|
||||
|
||||
def __getDetails(self):
|
||||
return self.__details
|
||||
|
||||
def __getName(self):
|
||||
return self.details.get("Core","Name")
|
||||
|
||||
def __setName(self, name):
|
||||
if not self.details.has_section("Core"):
|
||||
self.details.add_section("Core")
|
||||
self.details.set("Core","Name",name)
|
||||
|
||||
|
||||
def __getPath(self):
|
||||
return self.details.get("Core","Module")
|
||||
|
||||
def __setPath(self,path):
|
||||
if not self.details.has_section("Core"):
|
||||
self.details.add_section("Core")
|
||||
self.details.set("Core","Module",path)
|
||||
|
||||
|
||||
def __getVersion(self):
|
||||
return StrictVersion(self.details.get("Documentation","Version"))
|
||||
|
||||
def setVersion(self, vstring):
|
||||
"""
|
||||
Set the version of the plugin.
|
||||
|
||||
Used by subclasses to provide different handling of the
|
||||
version number.
|
||||
"""
|
||||
if isinstance(vstring,StrictVersion):
|
||||
vstring = str(vstring)
|
||||
if not self.details.has_section("Documentation"):
|
||||
self.details.add_section("Documentation")
|
||||
self.details.set("Documentation","Version",vstring)
|
||||
|
||||
def __getAuthor(self):
|
||||
return self.details.get("Documentation","Author")
|
||||
|
||||
def __setAuthor(self,author):
|
||||
if not self.details.has_section("Documentation"):
|
||||
self.details.add_section("Documentation")
|
||||
self.details.set("Documentation","Author",author)
|
||||
|
||||
|
||||
def __getCopyright(self):
|
||||
return self.details.get("Documentation","Copyright")
|
||||
|
||||
def __setCopyright(self,copyrightTxt):
|
||||
if not self.details.has_section("Documentation"):
|
||||
self.details.add_section("Documentation")
|
||||
self.details.set("Documentation","Copyright",copyrightTxt)
|
||||
|
||||
|
||||
def __getWebsite(self):
|
||||
return self.details.get("Documentation","Website")
|
||||
|
||||
def __setWebsite(self,website):
|
||||
if not self.details.has_section("Documentation"):
|
||||
self.details.add_section("Documentation")
|
||||
self.details.set("Documentation","Website",website)
|
||||
|
||||
|
||||
def __getDescription(self):
|
||||
return self.details.get("Documentation","Description")
|
||||
|
||||
def __setDescription(self,description):
|
||||
if not self.details.has_section("Documentation"):
|
||||
self.details.add_section("Documentation")
|
||||
return self.details.set("Documentation","Description",description)
|
||||
|
||||
|
||||
def __getCategory(self):
|
||||
"""
|
||||
DEPRECATED (>1.9): Mimic former behaviour when what is
|
||||
noz the first category was considered as the only one the
|
||||
plugin belonged to.
|
||||
"""
|
||||
if self.categories:
|
||||
return self.categories[0]
|
||||
else:
|
||||
return "UnknownCategory"
|
||||
|
||||
def __setCategory(self,c):
|
||||
"""
|
||||
DEPRECATED (>1.9): Mimic former behaviour by making so
|
||||
that if a category is set as if it were the only category to
|
||||
which the plugin belongs, then a __getCategory will return
|
||||
this newly set category.
|
||||
"""
|
||||
self.categories = [c] + self.categories
|
||||
|
||||
name = property(fget=__getName,fset=__setName)
|
||||
path = property(fget=__getPath,fset=__setPath)
|
||||
version = property(fget=__getVersion,fset=setVersion)
|
||||
author = property(fget=__getAuthor,fset=__setAuthor)
|
||||
copyright = property(fget=__getCopyright,fset=__setCopyright)
|
||||
website = property(fget=__getWebsite,fset=__setWebsite)
|
||||
description = property(fget=__getDescription,fset=__setDescription)
|
||||
details = property(fget=__getDetails,fset=__setDetails)
|
||||
# deprecated (>1.9): plugins are not longer associated to a
|
||||
# single category !
|
||||
category = property(fget=__getCategory,fset=__setCategory)
|
||||
|
||||
def _getIsActivated(self):
|
||||
"""
|
||||
Return the activated state of the plugin object.
|
||||
Makes it possible to define a property.
|
||||
"""
|
||||
return self.plugin_object.is_activated
|
||||
|
||||
is_activated = property(fget=_getIsActivated)
|
||||
|
||||
def _ensureDetailsDefaultsAreBackwardCompatible(self):
|
||||
"""
|
||||
Internal helper function.
|
||||
"""
|
||||
if not self.details.has_option("Documentation","Author"):
|
||||
self.author = "Unknown"
|
||||
if not self.details.has_option("Documentation","Version"):
|
||||
self.version = "0.0"
|
||||
if not self.details.has_option("Documentation","Website"):
|
||||
self.website = "None"
|
||||
if not self.details.has_option("Documentation","Copyright"):
|
||||
self.copyright = "Unknown"
|
||||
if not self.details.has_option("Documentation","Description"):
|
||||
self.description = ""
|
740
yapsy/PluginManager.py
Normal file
740
yapsy/PluginManager.py
Normal file
@@ -0,0 +1,740 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
The ``PluginManager`` loads plugins that enforce the `Plugin
|
||||
Description Policy`_, and offers the most simple methods to activate
|
||||
and deactivate the plugins once they are loaded.
|
||||
|
||||
.. note:: It may also classify the plugins in various categories, but
|
||||
this behaviour is optional and if not specified elseway all
|
||||
plugins are stored in the same default category.
|
||||
|
||||
.. note:: It is often more useful to have the plugin manager behave
|
||||
like singleton, this functionality is provided by
|
||||
``PluginManagerSingleton``
|
||||
|
||||
|
||||
Plugin Description Policy
|
||||
=========================
|
||||
|
||||
When creating a ``PluginManager`` instance, one should provide it with
|
||||
a list of directories where plugins may be found. In each directory,
|
||||
a plugin should contain the following elements:
|
||||
|
||||
For a *Standard* plugin:
|
||||
|
||||
``myplugin.yapsy-plugin``
|
||||
|
||||
A *plugin info file* identical to the one previously described.
|
||||
|
||||
``myplugin``
|
||||
|
||||
A directory ontaining an actual Python plugin (ie with a
|
||||
``__init__.py`` file that makes it importable). The upper
|
||||
namespace of the plugin should present a class inheriting the
|
||||
``IPlugin`` interface (the same remarks apply here as in the
|
||||
previous case).
|
||||
|
||||
|
||||
For a *Single file* plugin:
|
||||
|
||||
``myplugin.yapsy-plugin``
|
||||
|
||||
A *plugin info file* which is identified thanks to its extension,
|
||||
see the `Plugin Info File Format`_ to see what should be in this
|
||||
file.
|
||||
|
||||
The extension is customisable at the ``PluginManager``'s
|
||||
instanciation, since one may usually prefer the extension to bear
|
||||
the application name.
|
||||
|
||||
``myplugin.py``
|
||||
|
||||
The source of the plugin. This file should at least define a class
|
||||
inheriting the ``IPlugin`` interface. This class will be
|
||||
instanciated at plugin loading and it will be notified the
|
||||
activation/deactivation events.
|
||||
|
||||
|
||||
Plugin Info File Format
|
||||
-----------------------
|
||||
|
||||
The plugin info file is a text file *encoded in ASCII or UTF-8* and
|
||||
gathering, as its name suggests, some basic information about the
|
||||
plugin.
|
||||
|
||||
- it gives crucial information needed to be able to load the plugin
|
||||
|
||||
- it provides some documentation like information like the plugin
|
||||
author's name and a short description fo the plugin functionality.
|
||||
|
||||
Here is an example of what such a file should contain::
|
||||
|
||||
[Core]
|
||||
Name = My plugin Name
|
||||
Module = the_name_of_the_pluginto_load_with_no_py_ending
|
||||
|
||||
[Documentation]
|
||||
Description = What my plugin broadly does
|
||||
Author = My very own name
|
||||
Version = the_version_number_of_the_plugin
|
||||
Website = My very own website
|
||||
|
||||
|
||||
|
||||
.. note:: From such plugin descriptions, the ``PluginManager`` will
|
||||
built its own representations of the plugins as instances of
|
||||
the :doc:`PluginInfo` class.
|
||||
|
||||
Changing the default behaviour
|
||||
==============================
|
||||
|
||||
The default behaviour for locating and loading plugins can be changed
|
||||
using the various options exposed on the interface via getters.
|
||||
|
||||
The plugin detection, in particular, can be fully customized by
|
||||
settting a custom plugin locator. See ``IPluginLocator`` for more
|
||||
details on this.
|
||||
|
||||
|
||||
Extensibility
|
||||
=============
|
||||
|
||||
Several mechanisms have been put up to help extending the basic
|
||||
functionalities of the proivided classes.
|
||||
|
||||
A few *hints* to help you extend those classes:
|
||||
|
||||
If the new functionalities do not overlap the ones already
|
||||
implemented, then they should be implemented as a Decorator class of the
|
||||
base plugin. This should be done by inheriting the
|
||||
``PluginManagerDecorator``.
|
||||
|
||||
If this previous way is not possible, then the functionalities should
|
||||
be added as a subclass of ``PluginManager``.
|
||||
|
||||
.. note:: The first method is highly prefered since it makes it
|
||||
possible to have a more flexible design where one can pick
|
||||
several functionalities and litterally *add* them to get an
|
||||
object corresponding to one's precise needs.
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
import importlib.abc.Loader as imp
|
||||
except ImportError:
|
||||
import imp
|
||||
|
||||
from yapsy import log
|
||||
from yapsy import NormalizePluginNameForModuleName
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.IPluginLocator import IPluginLocator
|
||||
# The follozing two imports are used to implement the default behaviour
|
||||
from yapsy.PluginFileLocator import PluginFileAnalyzerWithInfoFile
|
||||
from yapsy.PluginFileLocator import PluginFileLocator
|
||||
# imported for backward compatibility (this variable was defined here
|
||||
# before 1.10)
|
||||
from yapsy import PLUGIN_NAME_FORBIDEN_STRING
|
||||
# imported for backward compatibility (this PluginInfo was imported
|
||||
# here before 1.10)
|
||||
from yapsy.PluginInfo import PluginInfo
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
"""
|
||||
Manage several plugins by ordering them in categories.
|
||||
|
||||
The mechanism for searching and loading the plugins is already
|
||||
implemented in this class so that it can be used directly (hence
|
||||
it can be considered as a bit more than a mere interface)
|
||||
|
||||
The file describing a plugin must be written in the syntax
|
||||
compatible with Python's ConfigParser module as in the
|
||||
`Plugin Info File Format`_
|
||||
|
||||
About the __init__:
|
||||
|
||||
Initialize the mapping of the categories and set the list of
|
||||
directories where plugins may be. This can also be set by
|
||||
direct call the methods:
|
||||
|
||||
- ``setCategoriesFilter`` for ``categories_filter``
|
||||
- ``setPluginPlaces`` for ``directories_list``
|
||||
- ``setPluginInfoExtension`` for ``plugin_info_ext``
|
||||
|
||||
You may look at these function's documentation for the meaning
|
||||
of each corresponding arguments.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext=None,
|
||||
plugin_locator=None):
|
||||
# as a good practice we don't use mutable objects as default
|
||||
# values (these objects would become like static variables)
|
||||
# for function/method arguments, but rather use None.
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default":IPlugin}
|
||||
self.setCategoriesFilter(categories_filter)
|
||||
plugin_locator = self._locatorDecide(plugin_info_ext, plugin_locator)
|
||||
# plugin_locator could be either a dict defining strategies, or directly
|
||||
# an IPluginLocator object
|
||||
self.setPluginLocator(plugin_locator, directories_list)
|
||||
|
||||
def _locatorDecide(self, plugin_info_ext, plugin_locator):
|
||||
"""
|
||||
For backward compatibility, we kept the *plugin_info_ext* argument.
|
||||
Thus we may use it if provided. Returns the (possibly modified)
|
||||
*plugin_locator*.
|
||||
"""
|
||||
specific_info_ext = plugin_info_ext is not None
|
||||
specific_locator = plugin_locator is not None
|
||||
if not specific_info_ext and not specific_locator:
|
||||
# use the default behavior
|
||||
res = PluginFileLocator()
|
||||
elif not specific_info_ext and specific_locator:
|
||||
# plugin_info_ext not used
|
||||
res = plugin_locator
|
||||
elif not specific_locator and specific_info_ext:
|
||||
# plugin_locator not used, and plugin_info_ext provided
|
||||
# -> compatibility mode
|
||||
res = PluginFileLocator()
|
||||
res.setAnalyzers([PluginFileAnalyzerWithInfoFile("info_ext",plugin_info_ext)])
|
||||
elif specific_info_ext and specific_locator:
|
||||
# both provided... issue a warning that tells "plugin_info_ext"
|
||||
# will be ignored
|
||||
msg = ("Two incompatible arguments (%s) provided:",
|
||||
"'plugin_info_ext' and 'plugin_locator'). Ignoring",
|
||||
"'plugin_info_ext'.")
|
||||
raise ValueError(" ".join(msg) % self.__class__.__name__)
|
||||
return res
|
||||
|
||||
def setCategoriesFilter(self, categories_filter):
|
||||
"""
|
||||
Set the categories of plugins to be looked for as well as the
|
||||
way to recognise them.
|
||||
|
||||
The ``categories_filter`` first defines the various categories
|
||||
in which the plugins will be stored via its keys and it also
|
||||
defines the interface tha has to be inherited by the actual
|
||||
plugin class belonging to each category.
|
||||
"""
|
||||
self.categories_interfaces = categories_filter.copy()
|
||||
# prepare the mapping from categories to plugin lists
|
||||
self.category_mapping = {}
|
||||
# also maps the plugin info files (useful to avoid loading
|
||||
# twice the same plugin...)
|
||||
self._category_file_mapping = {}
|
||||
for categ in categories_filter:
|
||||
self.category_mapping[categ] = []
|
||||
self._category_file_mapping[categ] = []
|
||||
|
||||
|
||||
def setPluginPlaces(self, directories_list):
|
||||
"""
|
||||
DEPRECATED(>1.9): directly configure the IPluginLocator instance instead !
|
||||
|
||||
Convenience method (actually call the IPluginLocator method)
|
||||
"""
|
||||
self.getPluginLocator().setPluginPlaces(directories_list)
|
||||
|
||||
def updatePluginPlaces(self, directories_list):
|
||||
"""
|
||||
DEPRECATED(>1.9): directly configure the IPluginLocator instance instead !
|
||||
|
||||
Convenience method (actually call the IPluginLocator method)
|
||||
"""
|
||||
self.getPluginLocator().updatePluginPlaces(directories_list)
|
||||
|
||||
def setPluginInfoExtension(self, ext):
|
||||
"""
|
||||
DEPRECATED(>1.9): for backward compatibility. Directly configure the
|
||||
IPluginLocator instance instead !
|
||||
|
||||
.. warning:: This will only work if the strategy "info_ext" is
|
||||
active for locating plugins.
|
||||
"""
|
||||
try:
|
||||
self.getPluginLocator().setPluginInfoExtension(ext)
|
||||
except KeyError:
|
||||
log.error("Current plugin locator doesn't support setting the plugin info extension.")
|
||||
|
||||
def setPluginInfoClass(self, picls, strategies=None):
|
||||
"""
|
||||
DEPRECATED(>1.9): directly configure the IPluginLocator instance instead !
|
||||
|
||||
Convenience method (actually call self.getPluginLocator().setPluginInfoClass)
|
||||
|
||||
When using a ``PluginFileLocator`` you may restrict the
|
||||
strategies to which the change of PluginInfo class will occur
|
||||
by just giving the list of strategy names in the argument
|
||||
"strategies"
|
||||
"""
|
||||
if strategies:
|
||||
for name in strategies:
|
||||
self.getPluginLocator().setPluginInfoClass(picls, name)
|
||||
else:
|
||||
self.getPluginLocator().setPluginInfoClass(picls)
|
||||
|
||||
def getPluginInfoClass(self):
|
||||
"""
|
||||
DEPRECATED(>1.9): directly control that with the IPluginLocator
|
||||
instance instead !
|
||||
|
||||
Get the class that holds PluginInfo.
|
||||
"""
|
||||
return self.getPluginLocator().getPluginInfoClass()
|
||||
|
||||
def setPluginLocator(self, plugin_locator, dir_list=None, picls=None):
|
||||
"""
|
||||
Sets the strategy used to locate the basic information.
|
||||
|
||||
.. note:
|
||||
If a `dir_list` is provided it overrides the directory list
|
||||
that may have been previously set in the locator.
|
||||
|
||||
See :doc:`IPluginLocator` for the policy that `plugin_locator` must enforce.
|
||||
"""
|
||||
if isinstance(plugin_locator, IPluginLocator):
|
||||
self._plugin_locator = plugin_locator
|
||||
if dir_list is not None:
|
||||
self._plugin_locator.setPluginPlaces(dir_list)
|
||||
if picls is not None:
|
||||
self.setPluginInfoClass(picls)
|
||||
else:
|
||||
raise TypeError("Unexpected format for plugin_locator ('%s' is not an instance of IPluginLocator)" % plugin_locator)
|
||||
|
||||
def getPluginLocator(self):
|
||||
"""
|
||||
Grant direct access to the plugin locator.
|
||||
"""
|
||||
return self._plugin_locator
|
||||
|
||||
def _gatherCorePluginInfo(self, directory, plugin_info_filename):
|
||||
"""
|
||||
DEPRECATED(>1.9): please use a specific plugin
|
||||
locator if you need such information.
|
||||
|
||||
Gather the core information (name, and module to be loaded)
|
||||
about a plugin described by it's info file (found at
|
||||
'directory/filename').
|
||||
|
||||
Return an instance of ``PluginInfo`` and the
|
||||
config_parser used to gather the core data *in a tuple*, if the
|
||||
required info could be localised, else return ``(None,None)``.
|
||||
|
||||
.. note:: This is supposed to be used internally by subclasses
|
||||
and decorators.
|
||||
|
||||
"""
|
||||
return self.getPluginLocator().gatherCorePluginInfo(directory,plugin_info_filename)
|
||||
|
||||
def _getPluginNameAndModuleFromStream(self,infoFileObject,candidate_infofile="<buffered info>"):
|
||||
"""
|
||||
DEPRECATED(>1.9): please use a specific plugin
|
||||
locator if you need such information.
|
||||
|
||||
Extract the name and module of a plugin from the
|
||||
content of the info file that describes it and which
|
||||
is stored in infoFileObject.
|
||||
|
||||
.. note::
|
||||
Prefer using ``_gatherCorePluginInfo``
|
||||
instead, whenever possible...
|
||||
|
||||
.. warning::
|
||||
``infoFileObject`` must be a file-like
|
||||
object: either an opened file for instance or a string
|
||||
buffer wrapped in a StringIO instance as another
|
||||
example.
|
||||
|
||||
.. note::
|
||||
``candidate_infofile`` must be provided
|
||||
whenever possible to get better error messages.
|
||||
|
||||
Return a 3-uple with the name of the plugin, its
|
||||
module and the config_parser used to gather the core
|
||||
data *in a tuple*, if the required info could be
|
||||
localised, else return ``(None,None,None)``.
|
||||
|
||||
.. note::
|
||||
This is supposed to be used internally by subclasses
|
||||
and decorators.
|
||||
"""
|
||||
return self.getPluginLocator().getPluginNameAndModuleFromStream(infoFileObject, candidate_infofile)
|
||||
|
||||
|
||||
def getCategories(self):
|
||||
"""
|
||||
Return the list of all categories.
|
||||
"""
|
||||
return list(self.category_mapping.keys())
|
||||
|
||||
def removePluginFromCategory(self, plugin,category_name):
|
||||
"""
|
||||
Remove a plugin from the category where it's assumed to belong.
|
||||
"""
|
||||
self.category_mapping[category_name].remove(plugin)
|
||||
|
||||
|
||||
def appendPluginToCategory(self, plugin, category_name):
|
||||
"""
|
||||
Append a new plugin to the given category.
|
||||
"""
|
||||
self.category_mapping[category_name].append(plugin)
|
||||
|
||||
def getPluginsOfCategory(self, category_name):
|
||||
"""
|
||||
Return the list of all plugins belonging to a category.
|
||||
"""
|
||||
return self.category_mapping[category_name][:]
|
||||
|
||||
def getAllPlugins(self):
|
||||
"""
|
||||
Return the list of all plugins (belonging to all categories).
|
||||
"""
|
||||
allPlugins = set()
|
||||
for pluginsOfOneCategory in self.category_mapping.values():
|
||||
allPlugins.update(pluginsOfOneCategory)
|
||||
return list(allPlugins)
|
||||
|
||||
def getPluginsOf(self, **kwargs):
|
||||
"""
|
||||
Returns a set of plugins whose properties match the named arguments provided here along with their correspoding values.
|
||||
"""
|
||||
selectedPLugins = set()
|
||||
for plugin in self.getAllPlugins():
|
||||
for attrName in kwargs:
|
||||
if not hasattr(plugin, attrName):
|
||||
break
|
||||
attrValue = kwargs[attrName]
|
||||
pluginValue = getattr(plugin, attrName)
|
||||
if pluginValue == attrValue:
|
||||
continue
|
||||
if type(pluginValue) == type(attrValue):
|
||||
break
|
||||
try:
|
||||
if attrValue in pluginValue:
|
||||
continue
|
||||
except:
|
||||
break
|
||||
else:
|
||||
selectedPLugins.add(plugin)
|
||||
return selectedPLugins
|
||||
|
||||
def getPluginCandidates(self):
|
||||
"""
|
||||
Return the list of possible plugins.
|
||||
|
||||
Each possible plugin (ie a candidate) is described by a 3-uple:
|
||||
(info file path, python file path, plugin info instance)
|
||||
|
||||
.. warning: ``locatePlugins`` must be called before !
|
||||
"""
|
||||
if not hasattr(self, '_candidates'):
|
||||
raise RuntimeError("locatePlugins must be called before getPluginCandidates")
|
||||
return self._candidates[:]
|
||||
|
||||
def removePluginCandidate(self,candidateTuple):
|
||||
"""
|
||||
Remove a given candidate from the list of plugins that should be loaded.
|
||||
|
||||
The candidate must be represented by the same tuple described
|
||||
in ``getPluginCandidates``.
|
||||
|
||||
.. warning: ``locatePlugins`` must be called before !
|
||||
"""
|
||||
if not hasattr(self, '_candidates'):
|
||||
raise ValueError("locatePlugins must be called before removePluginCandidate")
|
||||
self._candidates.remove(candidateTuple)
|
||||
|
||||
def appendPluginCandidate(self, candidateTuple):
|
||||
"""
|
||||
Append a new candidate to the list of plugins that should be loaded.
|
||||
|
||||
The candidate must be represented by the same tuple described
|
||||
in ``getPluginCandidates``.
|
||||
|
||||
.. warning: ``locatePlugins`` must be called before !
|
||||
"""
|
||||
if not hasattr(self, '_candidates'):
|
||||
raise ValueError("locatePlugins must be called before removePluginCandidate")
|
||||
self._candidates.append(candidateTuple)
|
||||
|
||||
def locatePlugins(self):
|
||||
"""
|
||||
Convenience method (actually call the IPluginLocator method)
|
||||
"""
|
||||
self._candidates, npc = self.getPluginLocator().locatePlugins()
|
||||
|
||||
def loadPlugins(self, callback=None, callback_after=None):
|
||||
"""
|
||||
Load the candidate plugins that have been identified through a
|
||||
previous call to locatePlugins. For each plugin candidate
|
||||
look for its category, load it and store it in the appropriate
|
||||
slot of the ``category_mapping``.
|
||||
|
||||
You can specify 2 callbacks: callback, and callback_after. If either of these are passed a function, (in the case of callback), it will get called before each plugin load attempt and (for callback_after), after each
|
||||
attempt. The ``plugin_info`` instance is passed as an argument to
|
||||
each callback. This is meant to facilitate code that needs to run for each plugin, such as adding the directory it resides in to sys.path (so imports of other files in the plugin's directory work correctly). You can use callback_after to remove anything you added to the path.
|
||||
"""
|
||||
# print "%s.loadPlugins" % self.__class__
|
||||
if not hasattr(self, '_candidates'):
|
||||
raise ValueError("locatePlugins must be called before loadPlugins")
|
||||
|
||||
processed_plugins = []
|
||||
for candidate_infofile, candidate_filepath, plugin_info in self._candidates:
|
||||
# make sure to attribute a unique module name to the one
|
||||
# that is about to be loaded
|
||||
plugin_module_name_template = NormalizePluginNameForModuleName("yapsy_loaded_plugin_" + plugin_info.name) + "_%d"
|
||||
for plugin_name_suffix in range(len(sys.modules)):
|
||||
plugin_module_name = plugin_module_name_template % plugin_name_suffix
|
||||
if plugin_module_name not in sys.modules:
|
||||
break
|
||||
|
||||
# tolerance on the presence (or not) of the py extensions
|
||||
if candidate_filepath.endswith(".py"):
|
||||
candidate_filepath = candidate_filepath[:-3]
|
||||
# if a callback exists, call it before attempting to load
|
||||
# the plugin so that a message can be displayed to the
|
||||
# user
|
||||
if callback is not None:
|
||||
callback(plugin_info)
|
||||
# cover the case when the __init__ of a package has been
|
||||
# explicitely indicated
|
||||
if "__init__" in os.path.basename(candidate_filepath):
|
||||
candidate_filepath = os.path.dirname(candidate_filepath)
|
||||
try:
|
||||
candidate_module = PluginManager._importModule(plugin_module_name, candidate_filepath)
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
log.error("Unable to import plugin: %s" % candidate_filepath, exc_info=exc_info)
|
||||
plugin_info.error = exc_info
|
||||
processed_plugins.append(plugin_info)
|
||||
continue
|
||||
|
||||
processed_plugins.append(plugin_info)
|
||||
if "__init__" in os.path.basename(candidate_filepath):
|
||||
sys.path.remove(plugin_info.path)
|
||||
# now try to find and initialise the first subclass of the correct plugin interface
|
||||
last_failed_attempt_message = None
|
||||
for element, element_name in ((getattr(candidate_module,name),name) for name in dir(candidate_module)):
|
||||
plugin_info_reference = None
|
||||
for category_name in self.categories_interfaces:
|
||||
try:
|
||||
is_correct_subclass = issubclass(element, self.categories_interfaces[category_name])
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
log.debug("correct subclass tests failed for: %s in %s" % (element_name, candidate_filepath), exc_info=exc_info)
|
||||
continue
|
||||
if is_correct_subclass and element is not self.categories_interfaces[category_name]:
|
||||
current_category = category_name
|
||||
if candidate_infofile not in self._category_file_mapping[current_category]:
|
||||
# we found a new plugin: initialise it and search for the next one
|
||||
if not plugin_info_reference:
|
||||
try:
|
||||
plugin_info.plugin_object = self.instanciateElementWithImportInfo(element, element_name, plugin_module_name, candidate_filepath)
|
||||
plugin_info_reference = plugin_info
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
last_failed_attempt_message = "Unable to create plugin object: %s" % candidate_filepath
|
||||
log.debug(last_failed_attempt_message, exc_info=exc_info)
|
||||
plugin_info.error = exc_info
|
||||
break # If it didn't work once it wont again
|
||||
else:
|
||||
last_failed_attempt_message = None
|
||||
plugin_info.categories.append(current_category)
|
||||
self.category_mapping[current_category].append(plugin_info_reference)
|
||||
self._category_file_mapping[current_category].append(candidate_infofile)
|
||||
#Everything is loaded and instantiated for this plugin now
|
||||
if callback_after is not None:
|
||||
callback_after(plugin_info)
|
||||
else:
|
||||
if last_failed_attempt_message:
|
||||
log.error(last_failed_attempt_message, exc_info=plugin_info.error)
|
||||
|
||||
# Remove candidates list since we don't need them any more and
|
||||
# don't need to take up the space
|
||||
delattr(self, '_candidates')
|
||||
return processed_plugins
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _importModule(plugin_module_name, candidate_filepath):
|
||||
"""
|
||||
Import a module, trying either to find it as a single file or as a directory.
|
||||
|
||||
.. note:: Isolated and provided to be reused, but not to be reimplemented !
|
||||
"""
|
||||
# use imp to correctly load the plugin as a module
|
||||
if os.path.isdir(candidate_filepath):
|
||||
candidate_module = imp.load_module(plugin_module_name,None,candidate_filepath,("py","r",imp.PKG_DIRECTORY))
|
||||
else:
|
||||
with open(candidate_filepath+".py","r") as plugin_file:
|
||||
candidate_module = imp.load_module(plugin_module_name,plugin_file,candidate_filepath+".py",("py","r",imp.PY_SOURCE))
|
||||
return candidate_module
|
||||
|
||||
def instanciateElementWithImportInfo(self, element, element_name,
|
||||
plugin_module_name, candidate_filepath):
|
||||
"""Override this method to customize how plugins are instanciated.
|
||||
|
||||
.. note::
|
||||
This methods recieves the 'element' that is a candidate
|
||||
as the plugin's main file, but also enough information to reload
|
||||
its containing module and this element.
|
||||
"""
|
||||
return self.instanciateElement(element)
|
||||
|
||||
def instanciateElement(self, element):
|
||||
"""
|
||||
DEPRECATED(>1.11): reimplement instead ``instanciateElementWithImportInfo`` !
|
||||
|
||||
Override this method to customize how plugins are instanciated.
|
||||
|
||||
.. warning::
|
||||
This method is called only if
|
||||
``instanciateElementWithImportInfo`` has not been reimplemented !
|
||||
"""
|
||||
return element()
|
||||
|
||||
def collectPlugins(self):
|
||||
"""
|
||||
Walk through the plugins' places and look for plugins. Then
|
||||
for each plugin candidate look for its category, load it and
|
||||
stores it in the appropriate slot of the category_mapping.
|
||||
"""
|
||||
# print "%s.collectPlugins" % self.__class__
|
||||
self.locatePlugins()
|
||||
self.loadPlugins()
|
||||
|
||||
|
||||
def getPluginByName(self,name,category="Default"):
|
||||
"""
|
||||
Get the plugin correspoding to a given category and name
|
||||
"""
|
||||
if category in self.category_mapping:
|
||||
for item in self.category_mapping[category]:
|
||||
if item.name == name:
|
||||
return item
|
||||
return None
|
||||
|
||||
def activatePluginByName(self,name,category="Default"):
|
||||
"""
|
||||
Activate a plugin corresponding to a given category + name.
|
||||
"""
|
||||
pta_item = self.getPluginByName(name,category)
|
||||
if pta_item is not None:
|
||||
plugin_to_activate = pta_item.plugin_object
|
||||
if plugin_to_activate is not None:
|
||||
log.debug("Activating plugin: %s.%s"% (category,name))
|
||||
plugin_to_activate.activate()
|
||||
return plugin_to_activate
|
||||
return None
|
||||
|
||||
|
||||
def deactivatePluginByName(self,name,category="Default"):
|
||||
"""
|
||||
Desactivate a plugin corresponding to a given category + name.
|
||||
"""
|
||||
if category in self.category_mapping:
|
||||
plugin_to_deactivate = None
|
||||
for item in self.category_mapping[category]:
|
||||
if item.name == name:
|
||||
plugin_to_deactivate = item.plugin_object
|
||||
break
|
||||
if plugin_to_deactivate is not None:
|
||||
log.debug("Deactivating plugin: %s.%s"% (category,name))
|
||||
plugin_to_deactivate.deactivate()
|
||||
return plugin_to_deactivate
|
||||
return None
|
||||
|
||||
|
||||
class PluginManagerSingleton(object):
|
||||
"""
|
||||
Singleton version of the most basic plugin manager.
|
||||
|
||||
Being a singleton, this class should not be initialised explicitly
|
||||
and the ``get`` classmethod must be called instead.
|
||||
|
||||
To call one of this class's methods you have to use the ``get``
|
||||
method in the following way:
|
||||
``PluginManagerSingleton.get().themethodname(theargs)``
|
||||
|
||||
To set up the various coonfigurables variables of the
|
||||
PluginManager's behaviour please call explicitly the following
|
||||
methods:
|
||||
|
||||
- ``setCategoriesFilter`` for ``categories_filter``
|
||||
- ``setPluginPlaces`` for ``directories_list``
|
||||
- ``setPluginInfoExtension`` for ``plugin_info_ext``
|
||||
"""
|
||||
|
||||
__instance = None
|
||||
|
||||
__decoration_chain = None
|
||||
|
||||
def __init__(self):
|
||||
if self.__instance is not None:
|
||||
raise Exception("Singleton can't be created twice !")
|
||||
|
||||
def setBehaviour(self,list_of_pmd):
|
||||
"""
|
||||
Set the functionalities handled by the plugin manager by
|
||||
giving a list of ``PluginManager`` decorators.
|
||||
|
||||
This function shouldn't be called several time in a same
|
||||
process, but if it is only the first call will have an effect.
|
||||
|
||||
It also has an effect only if called before the initialisation
|
||||
of the singleton.
|
||||
|
||||
In cases where the function is indeed going to change anything
|
||||
the ``True`` value is return, in all other cases, the ``False``
|
||||
value is returned.
|
||||
"""
|
||||
if self.__decoration_chain is None and self.__instance is None:
|
||||
log.debug("Setting up a specific behaviour for the PluginManagerSingleton")
|
||||
self.__decoration_chain = list_of_pmd
|
||||
return True
|
||||
else:
|
||||
log.debug("Useless call to setBehaviour: the singleton is already instanciated of already has a behaviour.")
|
||||
return False
|
||||
setBehaviour = classmethod(setBehaviour)
|
||||
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Actually create an instance
|
||||
"""
|
||||
if self.__instance is None:
|
||||
if self.__decoration_chain is not None:
|
||||
# Get the object to be decorated
|
||||
# print self.__decoration_chain
|
||||
pm = self.__decoration_chain[0]()
|
||||
for cls_item in self.__decoration_chain[1:]:
|
||||
# print cls_item
|
||||
pm = cls_item(decorated_manager=pm)
|
||||
# Decorate the whole object
|
||||
self.__instance = pm
|
||||
else:
|
||||
# initialise the 'inner' PluginManagerDecorator
|
||||
self.__instance = PluginManager()
|
||||
log.debug("PluginManagerSingleton initialised")
|
||||
return self.__instance
|
||||
get = classmethod(get)
|
||||
|
||||
|
||||
# For backward compatility import the most basic decorator (it changed
|
||||
# place as of v1.8)
|
||||
from yapsy.PluginManagerDecorator import PluginManagerDecorator
|
||||
|
105
yapsy/PluginManagerDecorator.py
Normal file
105
yapsy/PluginManagerDecorator.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Provide an easy way to build a chain of decorators extending the
|
||||
functionalities of the default plugin manager, when it comes to
|
||||
activating, deactivating or looking into loaded plugins.
|
||||
|
||||
The ``PluginManagerDecorator`` is the base class to be inherited by
|
||||
each element of the chain of decorator.
|
||||
|
||||
.. warning:: If you want to customise the way the plugins are detected
|
||||
and loaded, you should not try to do it by implementing a
|
||||
new ``PluginManagerDecorator``. Instead, you'll have to
|
||||
reimplement the :doc:`PluginManager` itself. And if you
|
||||
do so by enforcing the ``PluginManager`` interface, just
|
||||
giving an instance of your new manager class to the
|
||||
``PluginManagerDecorator`` should be transparent to the
|
||||
"stantard" decorators.
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy import log
|
||||
|
||||
|
||||
class PluginManagerDecorator(object):
|
||||
"""
|
||||
Add several responsibilities to a plugin manager object in a
|
||||
more flexible way than by mere subclassing. This is indeed an
|
||||
implementation of the Decorator Design Patterns.
|
||||
|
||||
|
||||
There is also an additional mechanism that allows for the
|
||||
automatic creation of the object to be decorated when this object
|
||||
is an instance of PluginManager (and not an instance of its
|
||||
subclasses). This way we can keep the plugin managers creation
|
||||
simple when the user don't want to mix a lot of 'enhancements' on
|
||||
the base class.
|
||||
|
||||
|
||||
About the __init__:
|
||||
|
||||
Mimics the PluginManager's __init__ method and wraps an
|
||||
instance of this class into this decorator class.
|
||||
|
||||
- *If the decorated_object is not specified*, then we use the
|
||||
PluginManager class to create the 'base' manager, and to do
|
||||
so we will use the arguments: ``categories_filter``,
|
||||
``directories_list``, and ``plugin_info_ext`` or their
|
||||
default value if they are not given.
|
||||
- *If the decorated object is given*, these last arguments are
|
||||
simply **ignored** !
|
||||
|
||||
All classes (and especially subclasses of this one) that want
|
||||
to be a decorator must accept the decorated manager as an
|
||||
object passed to the init function under the exact keyword
|
||||
``decorated_object``.
|
||||
"""
|
||||
|
||||
def __init__(self, decorated_object=None,
|
||||
# The following args will only be used if we need to
|
||||
# create a default PluginManager
|
||||
categories_filter=None,
|
||||
directories_list=None,
|
||||
plugin_info_ext="yapsy-plugin"):
|
||||
if directories_list is None:
|
||||
directories_list = [os.path.dirname(__file__)]
|
||||
if categories_filter is None:
|
||||
categories_filter = {"Default": IPlugin}
|
||||
if decorated_object is None:
|
||||
log.debug("Creating a default PluginManager instance to be decorated.")
|
||||
from yapsy.PluginManager import PluginManager
|
||||
decorated_object = PluginManager(categories_filter,
|
||||
directories_list,
|
||||
plugin_info_ext)
|
||||
self._component = decorated_object
|
||||
|
||||
def __getattr__(self,name):
|
||||
"""
|
||||
Decorator trick copied from:
|
||||
http://www.pasteur.fr/formation/infobio/python/ch18s06.html
|
||||
"""
|
||||
# print "looking for %s in %s" % (name, self.__class__)
|
||||
return getattr(self._component,name)
|
||||
|
||||
|
||||
def collectPlugins(self):
|
||||
"""
|
||||
This function will usually be a shortcut to successively call
|
||||
``self.locatePlugins`` and then ``self.loadPlugins`` which are
|
||||
very likely to be redefined in each new decorator.
|
||||
|
||||
So in order for this to keep on being a "shortcut" and not a
|
||||
real pain, I'm redefining it here.
|
||||
"""
|
||||
self.locatePlugins()
|
||||
self.loadPlugins()
|
132
yapsy/VersionedPluginManager.py
Normal file
132
yapsy/VersionedPluginManager.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
Role
|
||||
====
|
||||
|
||||
Defines the basic interface for a plugin manager that also keeps track
|
||||
of versions of plugins
|
||||
|
||||
API
|
||||
===
|
||||
"""
|
||||
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from yapsy.PluginInfo import PluginInfo
|
||||
from yapsy.IPlugin import IPlugin
|
||||
from yapsy.PluginManagerDecorator import PluginManagerDecorator
|
||||
|
||||
|
||||
class VersionedPluginInfo(PluginInfo):
|
||||
"""
|
||||
Gather some info about a plugin such as its name, author,
|
||||
description...
|
||||
"""
|
||||
|
||||
def __init__(self, plugin_name, plugin_path):
|
||||
PluginInfo.__init__(self, plugin_name, plugin_path)
|
||||
# version number is now required to be a StrictVersion object
|
||||
self.version = StrictVersion("0.0")
|
||||
|
||||
def setVersion(self, vstring):
|
||||
self.version = StrictVersion(vstring)
|
||||
|
||||
|
||||
class VersionedPluginManager(PluginManagerDecorator):
|
||||
"""
|
||||
Handle plugin versioning by making sure that when several
|
||||
versions are present for a same plugin, only the latest version is
|
||||
manipulated via the standard methods (eg for activation and
|
||||
deactivation)
|
||||
|
||||
More precisely, for operations that must be applied on a single
|
||||
named plugin at a time (``getPluginByName``,
|
||||
``activatePluginByName``, ``deactivatePluginByName`` etc) the
|
||||
targetted plugin will always be the one with the latest version.
|
||||
|
||||
.. note:: The older versions of a given plugin are still reachable
|
||||
via the ``getPluginsOfCategoryFromAttic`` method.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
decorated_manager=None,
|
||||
categories_filter={"Default":IPlugin},
|
||||
directories_list=None,
|
||||
plugin_info_ext="yapsy-plugin"):
|
||||
# Create the base decorator class
|
||||
PluginManagerDecorator.__init__(self,decorated_manager,
|
||||
categories_filter,
|
||||
directories_list,
|
||||
plugin_info_ext)
|
||||
self.setPluginInfoClass(VersionedPluginInfo)
|
||||
# prepare the storage for the early version of the plugins,
|
||||
# for which only the latest version is the one that will be
|
||||
# kept in the "core" plugin storage.
|
||||
self._prepareAttic()
|
||||
|
||||
def _prepareAttic(self):
|
||||
"""
|
||||
Create and correctly initialize the storage where the wrong
|
||||
version of the plugins will be stored.
|
||||
"""
|
||||
self._attic = {}
|
||||
for categ in self.getCategories():
|
||||
self._attic[categ] = []
|
||||
|
||||
def setCategoriesFilter(self, categories_filter):
|
||||
"""
|
||||
Set the categories of plugins to be looked for as well as the
|
||||
way to recognise them.
|
||||
|
||||
Note: will also reset the attic toa void inconsistencies.
|
||||
"""
|
||||
self._component.setCategoriesFilter(categories_filter)
|
||||
self._prepareAttic()
|
||||
|
||||
def getLatestPluginsOfCategory(self,category_name):
|
||||
"""
|
||||
DEPRECATED(>1.8): Please consider using getPluginsOfCategory
|
||||
instead.
|
||||
|
||||
Return the list of all plugins belonging to a category.
|
||||
"""
|
||||
return self.getPluginsOfCategory(category_name)
|
||||
|
||||
def loadPlugins(self, callback=None, callback_after=None):
|
||||
"""
|
||||
Load the candidate plugins that have been identified through a
|
||||
previous call to locatePlugins.
|
||||
|
||||
In addition to the baseclass functionality, this subclass also
|
||||
needs to find the latest version of each plugin.
|
||||
"""
|
||||
self._prepareAttic()
|
||||
self._component.loadPlugins(callback, callback_after)
|
||||
for categ in self.getCategories():
|
||||
latest_plugins = {}
|
||||
allPlugins = self.getPluginsOfCategory(categ)
|
||||
# identify the latest version of each plugin
|
||||
for plugin in allPlugins:
|
||||
name = plugin.name
|
||||
version = plugin.version
|
||||
if name in latest_plugins:
|
||||
if version > latest_plugins[name].version:
|
||||
older_plugin = latest_plugins[name]
|
||||
latest_plugins[name] = plugin
|
||||
self.removePluginFromCategory(older_plugin,categ)
|
||||
self._attic[categ].append(older_plugin)
|
||||
else:
|
||||
self.removePluginFromCategory(plugin,categ)
|
||||
self._attic[categ].append(plugin)
|
||||
else:
|
||||
latest_plugins[name] = plugin
|
||||
|
||||
def getPluginsOfCategoryFromAttic(self,categ):
|
||||
"""
|
||||
Access the older version of plugins for which only the latest
|
||||
version is available through standard methods.
|
||||
"""
|
||||
return self._attic[categ]
|
||||
|
97
yapsy/__init__.py
Normal file
97
yapsy/__init__.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Yapsy's main purpose is to offer a way to easily design a plugin
|
||||
system in Python, and motivated by the fact that many other Python
|
||||
plugin system are either too complicated for a basic use or depend on
|
||||
a lot of libraries. Yapsy only depends on Python's standard library.
|
||||
|
||||
|yapsy| basically defines two core classes:
|
||||
|
||||
- a fully functional though very simple ``PluginManager`` class
|
||||
|
||||
- an interface ``IPlugin`` which defines the interface of plugin
|
||||
instances handled by the ``PluginManager``
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
The basic classes defined by |yapsy| should work "as is" and enable
|
||||
you to load and activate your plugins. So that the following code
|
||||
should get you a fully working plugin management system::
|
||||
|
||||
from yapsy.PluginManager import PluginManager
|
||||
|
||||
# Build the manager
|
||||
simplePluginManager = PluginManager()
|
||||
# Tell it the default place(s) where to find plugins
|
||||
simplePluginManager.setPluginPlaces(["path/to/myplugins"])
|
||||
# Load all plugins
|
||||
simplePluginManager.collectPlugins()
|
||||
|
||||
# Activate all loaded plugins
|
||||
for pluginInfo in simplePluginManager.getAllPlugins():
|
||||
simplePluginManager.activatePluginByName(pluginInfo.name)
|
||||
|
||||
|
||||
.. note:: The ``plugin_info`` object (typically an instance of
|
||||
``IPlugin``) plays as *the entry point of each
|
||||
plugin*. That's also where |yapsy| ceases to guide you: it's
|
||||
up to you to define what your plugins can do and how you
|
||||
want to talk to them ! Talking to your plugin will then look
|
||||
very much like the following::
|
||||
|
||||
# Trigger 'some action' from the loaded plugins
|
||||
for pluginInfo in simplePluginManager.getAllPlugins():
|
||||
pluginInfo.plugin_object.doSomething(...)
|
||||
|
||||
"""
|
||||
|
||||
__version__="1.12.2"
|
||||
|
||||
# tell epydoc that the documentation is in the reStructuredText format
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
# provide a default named log for package-wide use
|
||||
import logging
|
||||
log = logging.getLogger('yapsy')
|
||||
|
||||
# Some constants concerning the plugins
|
||||
PLUGIN_NAME_FORBIDEN_STRING=";;"
|
||||
"""
|
||||
.. warning:: This string (';;' by default) is forbidden in plugin
|
||||
names, and will be usable to describe lists of plugins
|
||||
for instance (see :doc:`ConfigurablePluginManager`)
|
||||
"""
|
||||
|
||||
import re
|
||||
from yapsy.compat import is_py2, str
|
||||
|
||||
if is_py2:
|
||||
RE_NON_ALPHANUM = re.compile("\W", re.U)
|
||||
else:
|
||||
RE_NON_ALPHANUM = re.compile("\W")
|
||||
|
||||
|
||||
def NormalizePluginNameForModuleName(pluginName):
|
||||
"""
|
||||
Normalize a plugin name into a safer name for a module name.
|
||||
|
||||
.. note:: may do a little more modifications than strictly
|
||||
necessary and is not optimized for speed.
|
||||
"""
|
||||
if is_py2:
|
||||
pluginName = str(pluginName, 'utf-8')
|
||||
if len(pluginName)==0:
|
||||
return "_"
|
||||
if pluginName[0].isdigit():
|
||||
pluginName = "_" + pluginName
|
||||
ret = RE_NON_ALPHANUM.sub("_",pluginName)
|
||||
if is_py2:
|
||||
ret = ret.encode('utf-8')
|
||||
return ret
|
97
yapsy/__init___flymake.py
Normal file
97
yapsy/__init___flymake.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
|
||||
|
||||
"""
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Yapsy's main purpose is to offer a way to easily design a plugin
|
||||
system in Python, and motivated by the fact that many other Python
|
||||
plugin system are either too complicated for a basic use or depend on
|
||||
a lot of libraries. Yapsy only depends on Python's standard library.
|
||||
|
||||
|yapsy| basically defines two core classes:
|
||||
|
||||
- a fully functional though very simple ``PluginManager`` class
|
||||
|
||||
- an interface ``IPlugin`` which defines the interface of plugin
|
||||
instances handled by the ``PluginManager``
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
The basic classes defined by |yapsy| should work "as is" and enable
|
||||
you to load and activate your plugins. So that the following code
|
||||
should get you a fully working plugin management system::
|
||||
|
||||
from yapsy.PluginManager import PluginManager
|
||||
|
||||
# Build the manager
|
||||
simplePluginManager = PluginManager()
|
||||
# Tell it the default place(s) where to find plugins
|
||||
simplePluginManager.setPluginPlaces(["path/to/myplugins"])
|
||||
# Load all plugins
|
||||
simplePluginManager.collectPlugins()
|
||||
|
||||
# Activate all loaded plugins
|
||||
for pluginInfo in simplePluginManager.getAllPlugins():
|
||||
simplePluginManager.activatePluginByName(pluginInfo.name)
|
||||
|
||||
|
||||
.. note:: The ``plugin_info`` object (typically an instance of
|
||||
``IPlugin``) plays as *the entry point of each
|
||||
plugin*. That's also where |yapsy| ceases to guide you: it's
|
||||
up to you to define what your plugins can do and how you
|
||||
want to talk to them ! Talking to your plugin will then look
|
||||
very much like the following::
|
||||
|
||||
# Trigger 'some action' from the loaded plugins
|
||||
for pluginInfo in simplePluginManager.getAllPlugins():
|
||||
pluginInfo.plugin_object.doSomething(...)
|
||||
|
||||
"""
|
||||
|
||||
__version__="1.12.2"
|
||||
|
||||
# tell epydoc that the documentation is in the reStructuredText format
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
# provide a default named log for package-wide use
|
||||
import logging
|
||||
log = logging.getLogger('yapsy')
|
||||
|
||||
# Some constants concerning the plugins
|
||||
PLUGIN_NAME_FORBIDEN_STRING=";;"
|
||||
"""
|
||||
.. warning:: This string (';;' by default) is forbidden in plugin
|
||||
names, and will be usable to describe lists of plugins
|
||||
for instance (see :doc:`ConfigurablePluginManager`)
|
||||
"""
|
||||
|
||||
import re
|
||||
from yapsy.compat import is_py2, str
|
||||
|
||||
if is_py2:
|
||||
RE_NON_ALPHANUM = re.compile("\W", re.U)
|
||||
else:
|
||||
RE_NON_ALPHANUM = re.compile("\W")
|
||||
|
||||
|
||||
def NormalizePluginNameForModuleName(pluginName):
|
||||
"""
|
||||
Normalize a plugin name into a safer name for a module name.
|
||||
|
||||
.. note:: may do a little more modifications than strictly
|
||||
necessary and is not optimized for speed.
|
||||
"""
|
||||
if is_py2:
|
||||
pluginName = str(pluginName, 'utf-8')
|
||||
if len(pluginName)==0:
|
||||
return "_"
|
||||
if pluginName[0].isdigit():
|
||||
pluginName = "_" + pluginName
|
||||
ret = RE_NON_ALPHANUM.sub("_",pluginName)
|
||||
if is_py2:
|
||||
ret = ret.encode('utf-8')
|
||||
return ret
|
58
yapsy/compat.py
Normal file
58
yapsy/compat.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Adapted from pythoncompat https://github.com/kennethreitz/requests/blob/724a3889bcd26c318cd4063519e67581d3be5d7e/requests/compat.py
|
||||
|
||||
License (ISC):
|
||||
|
||||
Copyright (c) 2012 Kenneth Reitz.
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
# -------
|
||||
# Pythons
|
||||
# -------
|
||||
|
||||
# Syntax sugar.
|
||||
_ver = sys.version_info
|
||||
|
||||
#: Python 2.x?
|
||||
is_py2 = (_ver[0] == 2)
|
||||
|
||||
#: Python 3.x?
|
||||
is_py3 = (_ver[0] == 3)
|
||||
|
||||
|
||||
if is_py2:
|
||||
from StringIO import StringIO
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
builtin_str = str
|
||||
bytes = str
|
||||
str = unicode
|
||||
basestring = basestring
|
||||
numeric_types = (int, long, float)
|
||||
|
||||
|
||||
elif is_py3:
|
||||
from io import StringIO
|
||||
from configparser import ConfigParser
|
||||
|
||||
builtin_str = str
|
||||
str = str
|
||||
bytes = bytes
|
||||
basestring = (str, bytes)
|
||||
numeric_types = (int, float)
|
Reference in New Issue
Block a user