This commit is contained in:
2024-07-13 21:58:40 +02:00
commit b563d787d6
97 changed files with 7567 additions and 0 deletions

118
CHANGELOG.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
yapsy

87
artwork/LICENSE.txt Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
artwork/yapsy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

148
artwork/yapsy.svg Normal file
View 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
View 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

View File

@@ -0,0 +1,7 @@
AutoInstallPluginManager
========================
.. automodule:: yapsy.AutoInstallPluginManager
:members:
:undoc-members:

View File

@@ -0,0 +1,7 @@
ConfigurablePluginManager
=========================
.. automodule:: yapsy.ConfigurablePluginManager
:members:
:undoc-members:

45
doc/Extensions.rst Normal file
View 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

View File

@@ -0,0 +1,7 @@
FilteredPluginManager
=====================
.. automodule:: yapsy.FilteredPluginManager
:members:
:undoc-members:

View File

@@ -0,0 +1,10 @@
========================
IMultiprocessChildPlugin
========================
.. toctree::
:maxdepth: 2
.. automodule:: yapsy.IMultiprocessChildPlugin
:members:
:undoc-members:

7
doc/IPlugin.rst Normal file
View File

@@ -0,0 +1,7 @@
IPlugin
=======
.. automodule:: yapsy.IPlugin
:members:
:undoc-members:

6
doc/IPluginLocator.rst Normal file
View File

@@ -0,0 +1,6 @@
IPluginLocator
==============
.. automodule:: yapsy.IPluginLocator
:members:
:undoc-members:

88
doc/Makefile Normal file
View 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."

View File

@@ -0,0 +1,12 @@
=========================
MultiprocessPluginManager
=========================
.. toctree::
:maxdepth: 2
.. automodule:: yapsy.MultiprocessPluginManager
:members:
:undoc-members:

View File

@@ -0,0 +1,10 @@
=======================
MultiprocessPluginProxy
=======================
.. toctree::
:maxdepth: 2
.. automodule:: yapsy.MultiprocessPluginProxy
:members:
:undoc-members:

View File

@@ -0,0 +1,6 @@
PluginFileLocator
=================
.. automodule:: yapsy.PluginFileLocator
:members:
:undoc-members:

8
doc/PluginInfo.rst Normal file
View File

@@ -0,0 +1,8 @@
==========
PluginInfo
==========
.. automodule:: yapsy.PluginInfo
:members:
:undoc-members:

12
doc/PluginManager.rst Normal file
View File

@@ -0,0 +1,12 @@
=============
PluginManager
=============
.. toctree::
:maxdepth: 2
.. automodule:: yapsy.PluginManager
:members:
:undoc-members:

View File

@@ -0,0 +1,12 @@
======================
PluginManagerDecorator
======================
.. toctree::
:maxdepth: 2
.. automodule:: yapsy.PluginManagerDecorator
:members:
:undoc-members:

View File

@@ -0,0 +1,7 @@
VersionedPluginManager
======================
.. automodule:: yapsy.VersionedPluginManager
:members:
:undoc-members:

209
doc/conf.py Normal file
View 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
View 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
View 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
View 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)

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[egg_info]
tag_build =
tag_date = 0

69
setup.py Normal file
View 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
View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*-
"""
Gathers the unittests of yapsy
"""

View 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")

View 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)

View 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))

View 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))

View 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)

View 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!")

View 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)

View 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)

View 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!")

View 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!")

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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)

View 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

View 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)

Binary file not shown.

Binary file not shown.

View File

@@ -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

View 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)

View File

@@ -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
View 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,
])

View 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
View 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),
])

View 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
View 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),
])

View 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
View 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),
])

View 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
View 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
View 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),
])

View 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
View 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__)))))

View 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

View 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)

View 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[:]

View 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

View 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
View 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
View 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)

View 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()

View 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
View 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
View 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
View 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

View 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()

View 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
View 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
View 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
View 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)