├── .coveragerc ├── .github └── workflows │ └── automatic-tests.yml ├── .gitignore ├── .hgignore ├── .hgtags ├── .readthedocs.yml ├── README.md ├── package ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.txt ├── TODO.txt ├── artwork │ ├── LICENSE.txt │ ├── yapsy-big.png │ ├── yapsy-favicon.ico │ ├── yapsy-favicon.png │ ├── yapsy.png │ └── yapsy.svg ├── doc │ ├── Advices.rst │ ├── AutoInstallPluginManager.rst │ ├── ConfigurablePluginManager.rst │ ├── Extensions.rst │ ├── FilteredPluginManager.rst │ ├── IMultiprocessChildPlugin.rst │ ├── IPlugin.rst │ ├── IPluginLocator.rst │ ├── Makefile │ ├── MultiprocessPluginManager.rst │ ├── MultiprocessPluginProxy.rst │ ├── PluginFileLocator.rst │ ├── PluginInfo.rst │ ├── PluginManager.rst │ ├── PluginManagerDecorator.rst │ ├── VersionedPluginManager.rst │ ├── conf.py │ ├── index.rst │ └── make.bat ├── runtests.py ├── setup.py ├── test │ ├── __init__.py │ ├── plugins │ │ ├── ConfigPlugin.py │ │ ├── ErroneousPlugin.py │ │ ├── LegacyMultiprocessPlugin.py │ │ ├── SimpleMultiprocessPlugin.py │ │ ├── SimplePlugin.py │ │ ├── VersionedPlugin10.py │ │ ├── VersionedPlugin11.py │ │ ├── VersionedPlugin111.py │ │ ├── VersionedPlugin12.py │ │ ├── VersionedPlugin12a1.py │ │ ├── configplugin.yapsy-config-plugin │ │ ├── configplugin.yapsy-filter-plugin │ │ ├── erroneousplugin.yapsy-error-plugin │ │ ├── legacymultiprocessplugin.multiprocess-plugin │ │ ├── simplemultiprocessplugin.multiprocess-plugin │ │ ├── simpleplugin.yapsy-filter-plugin │ │ ├── simpleplugin.yapsy-plugin │ │ ├── versioned10.version-plugin │ │ ├── versioned11.version-plugin │ │ ├── versioned111.version-plugin │ │ ├── versioned12.version-plugin │ │ └── versioned12a1.version-plugin │ ├── pluginsasdirs │ │ ├── SimplePlugin │ │ │ └── __init__.py │ │ └── simpleplugin.yapsy-plugin │ ├── pluginstoinstall │ │ ├── AutoInstallPlugin.py │ │ ├── autoinstallWRONGzipplugin.zip │ │ ├── autoinstallZIPplugin.zip │ │ ├── autoinstalldirplugin.yapsy-autoinstall-plugin │ │ ├── autoinstalldirplugin │ │ │ └── __init__.py │ │ └── autoinstallplugin.yapsy-autoinstall-plugin │ ├── test_All.py │ ├── test_AutoInstallPlugin.py │ ├── test_ConfigPlugin.py │ ├── test_ErrorInPlugin.py │ ├── test_FilterPlugin.py │ ├── test_PluginFileLocator.py │ ├── test_PluginInfo.py │ ├── test_SimpleMultiprocessPlugin.py │ ├── test_SimplePlugin.py │ ├── test_Singleton.py │ ├── test_VersionedPlugin.py │ └── test_settings.py └── yapsy │ ├── AutoInstallPluginManager.py │ ├── ConfigurablePluginManager.py │ ├── FilteredPluginManager.py │ ├── IMultiprocessChildPlugin.py │ ├── IMultiprocessPlugin.py │ ├── IPlugin.py │ ├── IPluginLocator.py │ ├── MultiprocessPluginManager.py │ ├── MultiprocessPluginProxy.py │ ├── PluginFileLocator.py │ ├── PluginInfo.py │ ├── PluginManager.py │ ├── PluginManagerDecorator.py │ ├── VersionedPluginManager.py │ └── __init__.py ├── requirements-release.txt ├── requirements-tests.txt └── utils ├── gen_doc.sh ├── release_env.sh └── upload_packages.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = /tmp/*, _*,*/site-packages/*,*/test/*,*/distutils/* 3 | 4 | [report] 5 | exclude_lines = 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /.github/workflows/automatic-tests.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pytest 23 | pip install -r requirements-tests.txt 24 | - name: Lint with flake8 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 ./package --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with pytest with coverage report 31 | run: | 32 | pytest --cov --cov-report=lcov 33 | - name: Coveralls 34 | uses: coverallsapp/github-action@master 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | path-to-lcov: coverage.lcov 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.swp 3 | __pycache__ 4 | .coverage 5 | .installed.cfg 6 | .mr.developer.cfg 7 | bin/ 8 | plugins/ 9 | db/* 10 | log/* 11 | env3.4/ 12 | dist/ 13 | build/ 14 | *.egg-info/ 15 | .idea/ 16 | develop-eggs/ 17 | src/ 18 | doc/src 19 | _html/ 20 | eggs/ 21 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | \.pyc$ 2 | \.~$ 3 | 4 | # use glob syntax. 5 | syntax: glob 6 | _build/* 7 | .project 8 | package/build/* 9 | package/dist/* 10 | package/Yapsy.egg-info/* 11 | htmlcov/* 12 | .coverage 13 | release_env/* 14 | .direnv/* 15 | .envrc 16 | 17 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 27a222ccc739de48d114ae282f848aad27775904 SubverionImport 2 | 27a222ccc739de48d114ae282f848aad27775904 SubverionImport 3 | 0000000000000000000000000000000000000000 SubverionImport 4 | 27a222ccc739de48d114ae282f848aad27775904 SubversionImport 5 | 87ace7ce5663b6c662bcfa54818e099ffdcceb53 release_Yapsy-1.8 6 | 43a85ec50934b636dce6647eb5342b70b809ca20 release_Yapsy-1.9 7 | ee9987b833d65887487af38cecf0eb9ed6b871eb release_Yapsy-1.9-python3 8 | 991a7a83265127a463c845fa56ada2183cdaafe3 release_Yapsy-1.10 9 | 991a7a83265127a463c845fa56ada2183cdaafe3 release_Yapsy-1.10 10 | 0000000000000000000000000000000000000000 release_Yapsy-1.10 11 | 0000000000000000000000000000000000000000 release_Yapsy-1.10 12 | b934a474c2e8fa765bfda7bce134060184284872 release_Yapsy-1.10 13 | a5c62d9f560fa44bbf41fdd0d2c9ddc85144f637 release_Yapsy-1.10.1 14 | 7000b8072f00e42d9c448092bbd2149cc76e8d21 release_Yapsy-1.10-python3 15 | 0000000000000000000000000000000000000000 release_Yapsy-1.10-python3 16 | 0000000000000000000000000000000000000000 release_Yapsy-1.10-python3 17 | 87fbef4ba66ba9f98692e9b657479fe5daae12a2 release_Yapsy-1.10-python3 18 | f59fd5772939d6779725d0bb55b612ba5b254534 release_Yapsy-1.10.1-python3 19 | 5c0ff8646c2e1b6e0a4676b57676ed5adbe6b479 release_Yapsy-1.10.2 20 | c1f8228a9fd08bbffbfd8b6b8ecd1de5c2d9236f release_Yapsy-1.10.2-python3 21 | 95b58ee3f7f4cd225caf8ff84bf5808fbf52279a release_Yapsy-1.10.323 22 | 777d3daf4648d8395be90a2059c749bca191cbcd release_Yapsy-1.10.323-python3 23 | 9bc11370f4b364b4d7a977d900d850eb98b69a67 release_Yapsy-1.11.023 24 | 45f2588c6221d2ccf2b8937692be48af7e3ba54e release_Yapsy-1.10.423 25 | ce1bb3d8cb4d3bb3bac116cd0ba5165c624eaf53 release_Yapsy-1.11.223 26 | 2e9f7c40b53120a956fd612a8f454c7ffbd8b473 release_Yapsy-1.12.023 27 | cda16532447dfc74b0cac564fe099aaab71242a8 release_Yapsy-1.12.0 28 | 2e9f7c40b53120a956fd612a8f454c7ffbd8b473 release_Yapsy-1.12.023 29 | 0000000000000000000000000000000000000000 release_Yapsy-1.12.023 30 | cda16532447dfc74b0cac564fe099aaab71242a8 release_Yapsy-1.12.0 31 | 0000000000000000000000000000000000000000 release_Yapsy-1.12.0 32 | 0000000000000000000000000000000000000000 release_Yapsy-1.12.0 33 | c1566d5c5fb4f65c425b113dbe091990c7d4a18f release_Yapsy-1.12.0 34 | 851e4edcf10e37ea751b7e57980dbf26880f6418 release_Yapsy-1.12.2 35 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: package/doc/conf.py 12 | 13 | # Optionally build your docs in additional formats such as PDF and ePub 14 | formats: all 15 | 16 | # Optionally set the version of Python and requirements 17 | # required to build your docs 18 | python: 19 | version: 3 20 | install: 21 | - requirements: requirements-release.txt 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yapsy 2 | ===== 3 | 4 | A fat-free DIY Python plugin management toolkit. 5 | 6 | [![BSD license](https://img.shields.io/pypi/l/yapsy.svg)](./package/LICENSE.txt) 7 | [![PyPI version](http://img.shields.io/pypi/v/Yapsy.svg)](https://pypi.python.org/pypi/yapsy) 8 | [![Documentation Status](https://readthedocs.org/projects/yapsy/badge/?version=latest)](https://yapsy.readthedocs.io/en/latest/?badge=latest) 9 | [![Automatic Tests](https://github.com/tibonihoo/yapsy/actions/workflows/automatic-tests.yml/badge.svg)](https://github.com/tibonihoo/yapsy/actions/workflows/automatic-tests.yml) 10 | [![Coverage Status](https://coveralls.io/repos/tibonihoo/yapsy/badge.png?branch=master)](https://coveralls.io/r/tibonihoo/yapsy?branch=master) 11 | 12 | 13 | Get more info on: 14 | * [the detailed description](./package/README.txt) 15 | * [the website](http://yapsy.sourceforge.net/) 16 | * [ReadTheDoc](https://yapsy.readthedocs.org) 17 | 18 | 19 | Yapsy's development is hosted on [github](https://github.com/tibonihoo/yapsy). 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | version-2.0.0 [????] 2 | - code: Python2 support is dropped, compat.py and its variables are gone. 3 | 4 | version-1.12.0 [2018-09-02] 5 | - code: fix yapsy on python3.6 6 | - code: Make the test more robust to "unusual" unpacking of the module (see: https://sourceforge.net/p/yapsy/bugs/32/) 7 | - code: Protect against providing a single string to setPluginPlaces (see: https://sourceforge.net/p/yapsy/bugs/38/) 8 | - code: Enforce the exact directory list provided at construction time (see: https://sourceforge.net/p/yapsy/bugs/36/) 9 | - code: Make multiprocess plugin work on windows too ! (see: https://sourceforge.net/p/yapsy/bugs/33/) 10 | - code: add a filter-based getter selecting plugins on plugininfo properties (see: https://sourceforge.net/p/yapsy/feature-requests/16/) 11 | - code: Add callback_after argument to the LoadPlugins method in PluginManager (contrib https://sourceforge.net/p/yapsy/feature-requests/9/) 12 | - code: Rejecting a candidate should not be a warning (contrib Guillaume Binet: https://github.com/tibonihoo/yapsy/pull/7) 13 | - code: fix PluginFileLocator __init__ should assignment of plugin_info_cls (contrib Xuecheng Zhang: https://github.com/tibonihoo/yapsy/pull/8) 14 | 15 | version-1.11.223 [2015-06-25] 16 | - doc: minor doc fixes 17 | 18 | version-1.11.123 [2015-05-08] 19 | 20 | - code: Make _extractCorePluginInfo accept Unicode filenames (bug https://sourceforge.net/p/yapsy/bugs/30/) 21 | - code: fix default change trigger for ConfigurablePluginManager (see https://sourceforge.net/p/yapsy/support-requests/9/) 22 | 23 | version-1.11.023 [2015-04-05] 24 | 25 | - code: merge python3 and default branch (contrib delijati) 26 | - code: fix exception catching to support flask use case (contrib delijati: https://github.com/tibonihoo/yapsy/pull/4) 27 | - code: fix error reporting (contrib frmdstryr: https://github.com/tibonihoo/yapsy/pull/5) 28 | - code: allow plugins to run in separate processes (contrib pylanglois: https://github.com/tibonihoo/yapsy/pull/6) 29 | - code: fix dangerous usage of mutable objects as default arguments 30 | - doc: added a few badges 31 | - doc: added an example of fetching yapsy's development version with pip 32 | 33 | version-1.10.423 [2014-06-07] 34 | 35 | - code: Speed optimisation for the regexp compiled in __init__.py (see https://sourceforge.net/p/yapsy/patches/4/) 36 | - code: fix bug "Plugin detection doesn't follow symlinks" (see https://sourceforge.net/p/yapsy/bugs/19/) 37 | - doc: add links to coveralls.io for code coverage 38 | 39 | version-1.10.323 [2014-03-23] 40 | 41 | - code: fix PluginInfo properties (see https://sourceforge.net/p/yapsy/bugs/13/) 42 | - code: fix ConfigurablePluginManager.loadplugin ignore callback bug reported at https://sourceforge.net/p/yapsy/bugs/17/ 43 | - code: small improvement to the parse error handling (related to https://sourceforge.net/p/yapsy/bugs/12/) 44 | 45 | version-1.10.223 [2013-12-06] 46 | 47 | - packaging: version name change to comply with PEP440 and resolve pip install problems. 48 | - code: fix compatibility with python2.5 49 | 50 | version-1.10.2 [2013-05-22] 51 | 52 | - code: fix compatibility with python2.5 53 | - doc: add links to travis-ci and readthedocs.org 54 | - code: fix AutoInstall test failures [contrib. Agustin Henze] 55 | - code: replace deprecated methods usage (for Python3) 56 | 57 | version-1.10.1 [2013-01-13] 58 | 59 | - code: switch from exec to imp.load_module for plugin loading which also solves https://sourceforge.net/p/yapsy/bugs/9/ 60 | - doc: add explanation about plugin class detection caveat https://sourceforge.net/p/yapsy/bugs/8/ 61 | - code: fix unicode bug on python2 version, see https://sourceforge.net/p/yapsy/bugs/10/ 62 | 63 | version-1.10 [2012-12-18] 64 | 65 | - code: [contrib. Mathieu Havel] "plugin locators" allow to change the strategy to describe and locate plugins 66 | - code: [contrib. Mathieu Clabaut] multiple categories per plugin (cf https://bitbucket.org/matclab/yapsy-mcl) 67 | - code: [contrib. Mark Fickett] improve logging 68 | - code: Gather detailed information on plugin load error via a callback 69 | - 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 70 | - code: proper config of the default "plugin locator" can stop plugin detection from scanning a directory recursively 71 | - code: Enforce a same tab convention everywhere 72 | - doc: update list of project using yapsy 73 | - doc: highlight the existence of tutorial and link to these ones: 74 | - doc: be more helpful to users with an advice/troubleshooting page 75 | - doc: add a CHANGELOG.txt file 76 | 77 | version-1.9.2 [2012-07-15] 78 | 79 | - packaging fixes and strange version bumps to workaround pypi.python.org's version handling 80 | 81 | version-1.9 [2011-12-23] 82 | 83 | - ability to load zipped plugins 84 | - a separate development branch has been created where the focus is on the compatibility with python3 85 | - no more SVN repository (as advertised last year it wasn't kept in sync with the Mercurial repository, and it is now officially dead) 86 | - better logging of errors and debug infos 87 | - small doc improvement, especially to show how simple it is to interactwith the plugins once they are loaded 88 | 89 | version-1.8 [2010-09-26] 90 | 91 | - the documentation has been refactored and should now go "straight to the point" 92 | - the source control is now performed by Mercurial 93 | - Filtering manager to filter out plugins that must not be loaded, contributed by Roger Gammans 94 | - a getAllPlugins method has been added to the PluginManager to make it easier to access plugins when only the default category is defined 95 | - code has been slightly cleaned up and should now be easy to adapt to Python3 via the 2to3 tool. 96 | 97 | version-1.7 [2008-04-09] 98 | 99 | - WARNING: API BREAK ! the arguments for [de]activatePluginByName and getPluginByName are now the other way round: category,name -> name,category="Default" 100 | - new AutoInstall manager for automatically installing plugins by copying them in proper place 101 | - small improvements to generic code for plugin loading 102 | 103 | version-1.6 [2007-11-10] 104 | 105 | - fix major bug in ConfigurablePluginManager 106 | 107 | version-1.5 [2007-11-03] 108 | 109 | - separation of plugin loading into locate and load contributed by Rob McMullen 110 | - package with "Easy install" framework 111 | - new forge (https://sourceforge.net/p/yapsy) and independent repo from mathbench 112 | 113 | version-1.1 [2007-09-21] 114 | 115 | - VersionedPlugin manager contributed by Rob McMullen 116 | 117 | version-1.0 [2007-08-26] 118 | 119 | - basic implementation of a PluginManager 120 | - ConfigurablePlugin manager that can store information in a ConfigParser compatible file 121 | - singleton versions of these plugin managers. 122 | -------------------------------------------------------------------------------- /package/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Yapsy is provided under the BSD-2 clause license (see text below), 2 | with the following two exceptions: 3 | 4 | - the "yapsy" icons in artwork/ is licensed under the Creative Commons 5 | Attribution-Share Alike 3.0 by Thibauld Nion (see 6 | artwork/LICENSE.txt) 7 | 8 | 9 | -------------------- 10 | BSD 2-clause license 11 | -------------------- 12 | 13 | Copyright (c) 2007-2015, Thibauld Nion 14 | 15 | All rights reserved. 16 | 17 | 18 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 19 | 20 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 21 | * 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. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 27 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 28 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /package/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.txt 2 | include LICENSE.txt 3 | include CHANGELOG.txt 4 | include runtests.py 5 | 6 | recursive-include test *.py *-plugin *.zip 7 | recursive-include yapsy *.py 8 | 9 | recursive-include artwork * 10 | recursive-include doc * 11 | prune doc/_build 12 | -------------------------------------------------------------------------------- /package/README.txt: -------------------------------------------------------------------------------- 1 | Yapsy is a small library implementing the core mechanisms needed to 2 | build a plugin system into a wider application. 3 | 4 | The main purpose is to depend only on Python's standard libraries and 5 | to implement only the basic functionalities needed to detect, load and 6 | keep track of several plugins. It supports both Python 2 and 3. 7 | 8 | To use yapsy, make sure that the "yapsy" directory is in your Python 9 | loading path and just import the needed class from yapsy (e.g. "from 10 | yapsy.PluginManager import PluginManager"). 11 | 12 | To see more examples, you can have a look at the unit tests inside the 13 | "test" directory or at the "Showcase and tutorials" section of the 14 | documentation (https://yapsy.readthedocs.io/en/latest/#showcase-and-tutorials). 15 | 16 | Please let me know if you find this useful. 17 | 18 | Site of the project: https://yapsy.readthedocs.io 19 | 20 | List of Contributors: 21 | - Thibauld Nion 22 | - Rob McMullen 23 | - Roger Gammans 24 | - Mathieu Havel 25 | - Mathieu Clabaut 26 | - Mark Fickett 27 | - Agustin Henze 28 | - qitta 29 | - Roberto Alsina 30 | - Josip Delic (delijati) 31 | - frmdstryr 32 | - Pierre-Yves Langlois 33 | - Guillaume Binet (gbin) 34 | - Blake Oliver (Oliver2213) 35 | - Xuecheng Zhang (csuzhangxc) 36 | - Marc Brooks (mbrooks-public) 37 | - Ameya Vikram Singh (AmeyaVS) 38 | 39 | Contributions are welcome as pull requests, patches or tickets on github 40 | (https://github.com/tibonihoo/yapsy). 41 | -------------------------------------------------------------------------------- /package/TODO.txt: -------------------------------------------------------------------------------- 1 | ====== 2 | TODO 3 | ====== 4 | 5 | Next Release 6 | 7 | 8 | Later 9 | 10 | 11 | 12 | Next Refactoring 13 | 14 | - code: consider making the filter and versionned plugin into plugin manager child classes (instead of decorators), err or maybe strategies ? 15 | - code: find a correct design to make extending the plugin "loading" easier and chainable (policies/mixins, traits ?) 16 | - code: [feature req.] Reloadable configuration for plugins 17 | - code: [feature req.] Allow Decorators to extend gatherPluginInfo 18 | -------------------------------------------------------------------------------- /package/artwork/yapsy-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/artwork/yapsy-big.png -------------------------------------------------------------------------------- /package/artwork/yapsy-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/artwork/yapsy-favicon.ico -------------------------------------------------------------------------------- /package/artwork/yapsy-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/artwork/yapsy-favicon.png -------------------------------------------------------------------------------- /package/artwork/yapsy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/artwork/yapsy.png -------------------------------------------------------------------------------- /package/artwork/yapsy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 25 | 27 | 31 | 35 | 36 | 47 | 48 | 66 | 68 | 69 | 71 | image/svg+xml 72 | 74 | 75 | 76 | 77 | 82 | 86 | 90 | 100 | 104 | 114 | 124 | 125 | 130 | 131 | 148 | 149 | -------------------------------------------------------------------------------- /package/doc/Advices.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | General advices and troubleshooting 3 | =================================== 4 | 5 | .. contents:: 6 | :local: 7 | 8 | 9 | Getting code samples 10 | -------------------- 11 | 12 | Yapsy is used enough for your favorite search provider to have good 13 | chances of finding some examples of yapsy being used in the wild. 14 | 15 | However if you wonder how a specific functionality can be used, you 16 | can also look at the corresponding unit test (in the test folder 17 | packaged with yapsy's sources). 18 | 19 | 20 | Use the logging system 21 | ---------------------- 22 | 23 | Yapsy uses Python's standard ``logging`` module to record most 24 | important events and especially plugin loading failures. 25 | 26 | When developping an application based on yapsy, you'll benefit from 27 | looking at the 'debug' level logs, which can easily be done from your 28 | application code with the following snippet:: 29 | 30 | import logging 31 | logging.basicConfig(level=logging.DEBUG) 32 | 33 | Also, please note that yapsy uses a named logger for all its logs, so 34 | that you can selectively activage debug logs for yapsy with the 35 | following snippet:: 36 | 37 | import logging 38 | logging.getLogger('yapsy').setLevel(logging.DEBUG) 39 | 40 | 41 | Categorization by inheritance caveat 42 | ------------------------------------ 43 | 44 | If your application defines various categories of plugins with the yapsy's built-in mechanism for that, please keep in mind the following facts: 45 | 46 | - a plugin instance is attributed to a given category by looking if 47 | it is an instance, *even via a subclass*, of the class associated 48 | to this category; 49 | - a plugin may be attributed to several categories. 50 | 51 | Considering this, and if you consider using several categories, you 52 | should consider the following tips: 53 | 54 | - **don't associate any category to ``IPlugin``** (unless you want 55 | all plugins to be attributed to the corresponding category) 56 | - **design a specific subclass** of ``IPlugin`` for each category 57 | - if you want to regroup plugins of some categories into a common 58 | category: do this by attributing a subclass of ``IPlugin`` to the 59 | common category and attribute to the other categories specific 60 | subclasses to this intermediate mother class so that **the plugin 61 | class inheritance hierarchy reflects the hierarchy between 62 | categories** (and if you want something more complex that a 63 | hierarchy, you can consider using mixins). 64 | 65 | 66 | Plugin class detection caveat 67 | ----------------------------- 68 | 69 | There must be **only one plugin defined per module**. This means that 70 | you can't have two plugin description files pointing at the same 71 | module for instance. 72 | 73 | Because of the "categorization by inheritance" system, you **musn't 74 | directly import the subclass** of ``IPlugin`` in the main plugin file, 75 | instead import its containing module and make your plugin class 76 | inherit from ``ContainingModule.SpecificPluginClass`` as in the 77 | following example. 78 | 79 | The following code won't work (the class ``MyBasePluginClass`` will be 80 | detected as the plugin's implementation instead of ``MyPlugin``):: 81 | 82 | from myapp.plugintypes import MyBasePluginClass 83 | 84 | class MyPlugin(MyBasePluginClass): 85 | pass 86 | 87 | Instead you should do the following:: 88 | 89 | import myapp.plugintypes as plugintypes 90 | 91 | class MyPlugin(plugintypes.MyBasePluginClass): 92 | pass 93 | 94 | 95 | Plugin packaging 96 | ---------------- 97 | 98 | When packaging plugins in a distutils installer or as parts of an 99 | application (like for instance with `py2exe`), you may want to take 100 | care about the following points: 101 | 102 | - when you set specific directories where to look for plugins with a 103 | hardcoded path, be very carefully about the way you write these 104 | paths because depending on the cases **using ``__file__`` or 105 | relative paths may be unreliable**. For instance with py2exe, you 106 | may want to follow the tips from the `Where Am I FAQ`_. 107 | 108 | - you'd should either **package the plugins as plain Python modules or 109 | data files** (if you want to consider you application as the only 110 | module), either using the dedicated `setup` argument for `py2exe` or 111 | using distutils' `MANIFEST.in` 112 | 113 | - if you do package the plugins as data files, **make sure that their 114 | dependencies are correctly indicated as dependencies of your 115 | package** (or packaged with you application if you use `py2exe`). 116 | 117 | See also a more detailed example for py2exe on `Simon on Tech's Using python plugin scripts with py2exe`_. 118 | 119 | .. _`Where Am I FAQ`: http://www.py2exe.org/index.cgi/WhereAmI 120 | .. _`Simon on Tech's Using python plugin scripts with py2exe`: http://notinthestars.blogspot.com.es/2011/04/using-python-plugin-scripts-with-py2exe.html 121 | 122 | 123 | Code conventions 124 | ---------------- 125 | 126 | If you intend to modify yapsy's sources and to contribute patches 127 | back, please respect the following conventions: 128 | 129 | - CamelCase (upper camel case) for class names and functions 130 | - camelCase (lower camel case) for methods 131 | - UPPERCASE for global variables (with a few exceptions) 132 | - tabulations are used for indentation (and not spaces !) 133 | - unit-test each new functionality 134 | 135 | -------------------------------------------------------------------------------- /package/doc/AutoInstallPluginManager.rst: -------------------------------------------------------------------------------- 1 | AutoInstallPluginManager 2 | ======================== 3 | 4 | .. automodule:: yapsy.AutoInstallPluginManager 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /package/doc/ConfigurablePluginManager.rst: -------------------------------------------------------------------------------- 1 | ConfigurablePluginManager 2 | ========================= 3 | 4 | .. automodule:: yapsy.ConfigurablePluginManager 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /package/doc/Extensions.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Built-in Extensions 3 | =================== 4 | 5 | The followig ready-to-use classes give you this exact extra 6 | functionality you need for your plugin manager: 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | VersionedPluginManager 13 | ConfigurablePluginManager 14 | AutoInstallPluginManager 15 | FilteredPluginManager 16 | MultiprocessPluginManager 17 | 18 | 19 | The following item offer customization for the way plugins are 20 | described and detected: 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | 25 | PluginFileLocator 26 | 27 | 28 | If you want to build your own extensions, have a look at the following 29 | interfaces: 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | 34 | IPluginLocator 35 | PluginManagerDecorator 36 | 37 | If you want to isolate your plugins in separate processes with the 38 | ``MultiprocessPluginManager``, you should look at the following 39 | classes too: 40 | 41 | .. toctree:: 42 | :maxdepth: 1 43 | 44 | IMultiprocessChildPlugin 45 | MultiprocessPluginProxy 46 | -------------------------------------------------------------------------------- /package/doc/FilteredPluginManager.rst: -------------------------------------------------------------------------------- 1 | FilteredPluginManager 2 | ===================== 3 | 4 | .. automodule:: yapsy.FilteredPluginManager 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /package/doc/IMultiprocessChildPlugin.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | IMultiprocessChildPlugin 3 | ======================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | .. automodule:: yapsy.IMultiprocessChildPlugin 9 | :members: 10 | :undoc-members: 11 | -------------------------------------------------------------------------------- /package/doc/IPlugin.rst: -------------------------------------------------------------------------------- 1 | IPlugin 2 | ======= 3 | 4 | .. automodule:: yapsy.IPlugin 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /package/doc/IPluginLocator.rst: -------------------------------------------------------------------------------- 1 | IPluginLocator 2 | ============== 3 | 4 | .. automodule:: yapsy.IPluginLocator 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /package/doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in _build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in _build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in _build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in _build/qthelp, like this:" 63 | @echo "# qcollectiongenerator _build/qthelp/Yapsy.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile _build/qthelp/Yapsy.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in _build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 76 | @echo 77 | @echo "The overview file is in _build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in _build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in _build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /package/doc/MultiprocessPluginManager.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | MultiprocessPluginManager 3 | ========================= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. automodule:: yapsy.MultiprocessPluginManager 10 | :members: 11 | :undoc-members: 12 | 13 | -------------------------------------------------------------------------------- /package/doc/MultiprocessPluginProxy.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | MultiprocessPluginProxy 3 | ======================= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | .. automodule:: yapsy.MultiprocessPluginProxy 9 | :members: 10 | :undoc-members: 11 | -------------------------------------------------------------------------------- /package/doc/PluginFileLocator.rst: -------------------------------------------------------------------------------- 1 | PluginFileLocator 2 | ================= 3 | 4 | .. automodule:: yapsy.PluginFileLocator 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /package/doc/PluginInfo.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | PluginInfo 3 | ========== 4 | 5 | .. automodule:: yapsy.PluginInfo 6 | :members: 7 | :undoc-members: 8 | 9 | -------------------------------------------------------------------------------- /package/doc/PluginManager.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | PluginManager 3 | ============= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. automodule:: yapsy.PluginManager 10 | :members: 11 | :undoc-members: 12 | 13 | -------------------------------------------------------------------------------- /package/doc/PluginManagerDecorator.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | PluginManagerDecorator 3 | ====================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | 9 | .. automodule:: yapsy.PluginManagerDecorator 10 | :members: 11 | :undoc-members: 12 | 13 | -------------------------------------------------------------------------------- /package/doc/VersionedPluginManager.rst: -------------------------------------------------------------------------------- 1 | VersionedPluginManager 2 | ====================== 3 | 4 | .. automodule:: yapsy.VersionedPluginManager 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /package/doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Yapsy documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Aug 21 19:38:34 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | SRC_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) 17 | sys.path = [SRC_DIR] + sys.path 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.append(os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = 'Yapsy' 44 | copyright = '2007-2018, Thibauld Nion' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | 51 | import sys 52 | sys.path.insert(0,os.path.dirname(__file__)) 53 | import yapsy 54 | # The short X.Y version. 55 | version = yapsy.__version__ 56 | # The full version, including alpha/beta/rc tags. 57 | release = yapsy.__version__ 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of documents that shouldn't be included in the build. 70 | #unused_docs = [] 71 | 72 | # List of directories, relative to source directory, that shouldn't be searched 73 | # for source files. 74 | exclude_trees = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. Major themes that come with 100 | # Sphinx are currently 'default' and 'sphinxdoc'. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | html_theme_options = { 107 | "sidebarbgcolor" : "#777", 108 | "sidebarlinkcolor": "#e0cede", 109 | "relbarbgcolor" : "#999", 110 | "relbarlinkcolor": "#e0cede", 111 | "footerbgcolor" : "#777", 112 | "headtextcolor" : "#5c3566", 113 | "linkcolor": "#5c3566", 114 | } 115 | 116 | # Add any paths that contain custom themes here, relative to this directory. 117 | #html_theme_path = [] 118 | 119 | # The name for this set of Sphinx documents. If None, it defaults to 120 | # " v documentation". 121 | #html_title = None 122 | 123 | # A shorter title for the navigation bar. Default is the same as html_title. 124 | #html_short_title = None 125 | 126 | # The name of an image file (relative to this directory) to place at the top 127 | # of the sidebar. 128 | html_logo = os.path.join(SRC_DIR,"artwork","yapsy-big.png") 129 | 130 | # The name of an image file (within the static path) to use as favicon of the 131 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 132 | # pixels large. 133 | html_favicon = os.path.join(SRC_DIR,"artwork","yapsy-favicon.ico") 134 | 135 | # Add any paths that contain custom static files (such as style sheets) here, 136 | # relative to this directory. They are copied after the builtin static files, 137 | # so a file named "default.css" will overwrite the builtin "default.css". 138 | html_static_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_use_modindex = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | #html_use_opensearch = '' 171 | 172 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 173 | #html_file_suffix = '' 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = 'Yapsydoc' 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | # The paper size ('letter' or 'a4'). 182 | #latex_paper_size = 'letter' 183 | 184 | # The font size ('10pt', '11pt' or '12pt'). 185 | #latex_font_size = '10pt' 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'Yapsy.tex', 'Yapsy Documentation', 191 | 'Thibauld Nion', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_use_modindex = True 210 | -------------------------------------------------------------------------------- /package/doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Yapsy documentation master file, created by 2 | sphinx-quickstart on Sat Aug 21 19:38:34 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ================================ 7 | Yapsy: Yet Another Plugin SYstem 8 | ================================ 9 | 10 | *A simple plugin system for Python applications* 11 | 12 | 13 | .. |Yapsy| replace:: **Yapsy** 14 | .. |CC-BYSA| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png 15 | :alt: Creative Commons License 16 | 17 | 18 | Quick links: 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | IPlugin 24 | PluginManager 25 | PluginInfo 26 | Extensions 27 | Advices 28 | 29 | 30 | .. contents:: On this page 31 | :local: 32 | 33 | 34 | .. automodule:: yapsy 35 | :members: 36 | :undoc-members: 37 | 38 | .. _extend: 39 | 40 | Make it your own 41 | ================ 42 | 43 | For applications that require the plugins and their managers to be 44 | more sophisticated, several techniques make such enhancement easy. The 45 | following sections detail the most frequent needs for extensions 46 | and what you can do about it. 47 | 48 | 49 | More sophisticated plugin classes 50 | --------------------------------- 51 | 52 | You can define a plugin class with a richer interface than 53 | ``IPlugin``, so long as it inherits from IPlugin, it should work the 54 | same. The only thing you need to know is that the plugin instance is 55 | accessible via the ``PluginInfo`` instance from its 56 | ``PluginInfo.plugin_object``. 57 | 58 | 59 | It is also possible to define a wider variety of plugins, by defining 60 | as much subclasses of IPlugin. But in such a case you have to inform 61 | the manager about that before collecting plugins:: 62 | 63 | # Build the manager 64 | simplePluginManager = PluginManager() 65 | # Tell it the default place(s) where to find plugins 66 | simplePluginManager.setPluginPlaces(["path/to/myplugins"]) 67 | # Define the various categories corresponding to the different 68 | # kinds of plugins you have defined 69 | simplePluginManager.setCategoriesFilter({ 70 | "Playback" : IPlaybackPlugin, 71 | "SongInfo" : ISongInfoPlugin, 72 | "Visualization" : IVisualisation, 73 | }) 74 | 75 | 76 | .. note:: Communicating with the plugins belonging to a given category 77 | might then be achieved with some code looking like the 78 | following:: 79 | 80 | # Trigger 'some action' from the "Visualization" plugins 81 | for pluginInfo in simplePluginManager.getPluginsOfCategory("Visualization"): 82 | pluginInfo.plugin_object.doSomething(...) 83 | 84 | 85 | Enhance the plugin manager's interface 86 | -------------------------------------- 87 | 88 | To make the plugin manager more helpful to the other components of an 89 | application, you should consider decorating it. 90 | 91 | Actually a "template" for such decoration is provided as 92 | :doc:`PluginManagerDecorator`, which must be inherited in order to 93 | implement the right decorator for your application. 94 | 95 | Such decorators can be chained, so that you can take advantage of the ready-made decorators such as: 96 | 97 | :doc:`ConfigurablePluginManager` 98 | 99 | Implements a ``PluginManager`` that uses a configuration file to 100 | save the plugins to be activated by default and also grants access 101 | to this file to the plugins. 102 | 103 | 104 | :doc:`AutoInstallPluginManager` 105 | 106 | Automatically copy the plugin files to the right plugin directory. 107 | 108 | A full list of pre-implemented decorators is available at :doc:`Extensions`. 109 | 110 | 111 | Modify plugin descriptions and detections 112 | ----------------------------------------- 113 | 114 | By default, plugins are described by a text file called the plugin 115 | "info file" expected to have a ".yapsy-plugin" extension. 116 | 117 | You may want to use another way to describe and detect your 118 | application's plugin and happily yapsy (since version 1.10) makes it 119 | possible to provide the ``PluginManager`` with a custom strategy for 120 | plugin detection. 121 | 122 | See :doc:`IPluginLocator` for the required interface of such 123 | strategies and :doc:`PluginFileLocator` for a working example of such 124 | a detection strategy. 125 | 126 | 127 | Modify the way plugins are loaded 128 | --------------------------------- 129 | 130 | To tweak the plugin loading phase it is highly advised to re-implement 131 | your own manager class. 132 | 133 | 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:: 134 | 135 | # build and configure a specific manager 136 | baseManager = MyNewManager() 137 | # start decorating this manager to add some more responsibilities 138 | myFirstDecorator = AFirstPluginManagerDecorator(baseManager) 139 | # add even more stuff 140 | mySecondDecorator = ASecondPluginManagerDecorator(myFirstDecorator) 141 | 142 | .. note:: Some decorators have been implemented that modify the way 143 | plugins are loaded, this is however not the easiest way to 144 | do it and it makes it harder to build a chain of decoration 145 | that would include these decorators. Among those are 146 | :doc:`VersionedPluginManager` and 147 | :doc:`FilteredPluginManager` 148 | 149 | 150 | Showcase and tutorials 151 | ====================== 152 | 153 | |yapsy| 's development has been originally motivated by the MathBench_ 154 | project but it is now used in other (more advanced) projects like: 155 | 156 | - peppy_ : "an XEmacs-like editor in Python. Eventually. " 157 | - MysteryMachine_ : "an application for writing freeform games." 158 | - Aranduka_ : "A simple e-book manager and reader" 159 | - err_ : "a plugin based chatbot" 160 | - nikola_ : "a Static Site and Blog Generator" 161 | 162 | .. _MathBench: http://mathbench.sourceforge.net 163 | .. _peppy: http://www.flipturn.org/peppy/ 164 | .. _MysteryMachine: http://trac.backslashat.org/MysteryMachine 165 | .. _Aranduka: https://github.com/ralsina/aranduka 166 | .. _err: http://gbin.github.com/err/ 167 | .. _nikola: http://nikola.ralsina.com.ar/ 168 | 169 | Nowadays, the development is clearly motivated by such external projects and the enthusiast developpers who use the library. 170 | 171 | If you're interested in using yapsy, feel free to look into the following links: 172 | 173 | - :doc:`Advices` 174 | - `A minimal example on stackoverflow`_ 175 | - `Making your app modular: Yapsy`_ (applied to Qt apps) 176 | - `Python plugins with yapsy`_ (applied to GTK apps) 177 | 178 | .. _`Making your app modular: Yapsy`: http://ralsina.me/weblog/posts/BB923.html 179 | .. _`A minimal example on stackoverflow`: http://stackoverflow.com/questions/5333128/yapsy-minimal-example 180 | .. _`Python plugins with yapsy`: https://github.com/MicahCarrick/yapsy-gtk-example 181 | 182 | 183 | Development 184 | =========== 185 | 186 | 187 | Contributing or forking ? 188 | ------------------------- 189 | 190 | You're always welcome if you suggest any kind of enhancements, any new 191 | decorators or any new pluginmanager. Even more if there is some code 192 | coming with it though this is absolutely not compulsory. 193 | 194 | It is also really fine to *fork* the code ! In the past, some people 195 | found |yapsy| just good enough to be used as a "code base" for their 196 | own plugin system, which they evolved in a more or less incompatible 197 | way with the "original" |yapsy|, if you think about it, with such a 198 | small library this is actually a clever thing to do. 199 | 200 | In any case, please remember that just providing some feedback on where 201 | you're using |yapsy| (original or forked) and how it is useful to you, 202 | is in itself a appreciable contribution :) 203 | 204 | 205 | License 206 | ------- 207 | 208 | The work is placed under the simplified BSD_ license in order to make 209 | it as easy as possible to be reused in other projects. 210 | 211 | .. _BSD: http://www.opensource.org/licenses/bsd-license.php 212 | 213 | Please note that the icon is not under the same license but under the 214 | `Creative Common Attribution-ShareAlike`_ license. 215 | 216 | .. _`Creative Common Attribution-ShareAlike`: http://creativecommons.org/licenses/by-sa/3.0/ 217 | 218 | 219 | Forge 220 | ----- 221 | 222 | The project is hosted on `GitHub`_. 223 | 224 | * To use `pip for a development install`_ you can do something like:: 225 | 226 | pip install -e "git+https://github.com/tibonihoo/yapsy.git#egg=yapsy&subdirectory=package" 227 | 228 | * A development version of the documentation is available on `ReadTheDoc`_. 229 | 230 | 231 | .. _`GitHub`: https://github.com/tibonihoo/yapsy/ 232 | .. _`pip for a development install`: http://pip.readthedocs.org/en/latest/reference/pip_install.html#vcs-support 233 | .. _`ReadTheDoc`: https://yapsy.readthedocs.org 234 | 235 | 236 | 237 | References 238 | ---------- 239 | 240 | Other Python plugin systems already existed before |yapsy| and some 241 | have appeared after that. |yapsy|'s creation is by no mean a sign that 242 | these others plugin systems sucks :) It is just the results of me 243 | being slighlty lazy and as I had already a good idea of how a simple 244 | plugin system should look like, I wanted to implement my own 245 | [#older_systems]_. 246 | 247 | 248 | - setuptools_ seems to be designed to allow applications to have a 249 | plugin system. 250 | 251 | .. _setuptools: http://cheeseshop.python.org/pypi/setuptools 252 | 253 | 254 | - Sprinkles_ seems to be also quite lightweight and simple but just 255 | maybe too far away from the design I had in mind. 256 | 257 | .. _Sprinkles: http://termie.pbwiki.com/SprinklesPy 258 | 259 | 260 | - PlugBoard_ is certainly quite good also but too complex for me. It also 261 | depends on zope which considered what I want to do here is way too 262 | much. 263 | 264 | .. _PlugBoard: https://pypi.python.org/pypi/PlugBoard 265 | 266 | - `Marty Alchin's simple plugin framework`_ is a quite interesting 267 | description of a plugin architecture with code snippets as 268 | illustrations. 269 | 270 | .. _`Marty Alchin's simple plugin framework`: http://martyalchin.com/2008/jan/10/simple-plugin-framework/ 271 | 272 | - stevedor_ looks quite promising and actually seems to make 273 | setuptools relevant to build plugin systems. 274 | 275 | .. _stevedor: https://pypi.python.org/pypi/stevedore 276 | 277 | - You can look up more example on a `stackoverflow's discution about minimal plugin systems in Python`_ 278 | 279 | .. _`stackoverflow's discution about minimal plugin systems in Python`: http://stackoverflow.com/questions/932069/building-a-minimal-plugin-architecture-in-python 280 | 281 | 282 | .. [#older_systems] All the more because it seems that my modest 283 | design ideas slightly differ from what has been done in other 284 | libraries. 285 | 286 | Indices and tables 287 | ================== 288 | 289 | * :ref:`genindex` 290 | * :ref:`modindex` 291 | * :ref:`search` 292 | 293 | -------------------------------------------------------------------------------- /package/doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% . 7 | if NOT "%PAPER%" == "" ( 8 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 9 | ) 10 | 11 | if "%1" == "" goto help 12 | 13 | if "%1" == "help" ( 14 | :help 15 | echo.Please use `make ^` where ^ is one of 16 | echo. html to make standalone HTML files 17 | echo. dirhtml to make HTML files named index.html in directories 18 | echo. pickle to make pickle files 19 | echo. json to make JSON files 20 | echo. htmlhelp to make HTML files and a HTML help project 21 | echo. qthelp to make HTML files and a qthelp project 22 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 23 | echo. changes to make an overview over all changed/added/deprecated items 24 | echo. linkcheck to check all external links for integrity 25 | echo. doctest to run all doctests embedded in the documentation if enabled 26 | goto end 27 | ) 28 | 29 | if "%1" == "clean" ( 30 | for /d %%i in (_build\*) do rmdir /q /s %%i 31 | del /q /s _build\* 32 | goto end 33 | ) 34 | 35 | if "%1" == "html" ( 36 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html 37 | echo. 38 | echo.Build finished. The HTML pages are in _build/html. 39 | goto end 40 | ) 41 | 42 | if "%1" == "dirhtml" ( 43 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% _build/dirhtml 44 | echo. 45 | echo.Build finished. The HTML pages are in _build/dirhtml. 46 | goto end 47 | ) 48 | 49 | if "%1" == "pickle" ( 50 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle 51 | echo. 52 | echo.Build finished; now you can process the pickle files. 53 | goto end 54 | ) 55 | 56 | if "%1" == "json" ( 57 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json 58 | echo. 59 | echo.Build finished; now you can process the JSON files. 60 | goto end 61 | ) 62 | 63 | if "%1" == "htmlhelp" ( 64 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% _build/htmlhelp 65 | echo. 66 | echo.Build finished; now you can run HTML Help Workshop with the ^ 67 | .hhp project file in _build/htmlhelp. 68 | goto end 69 | ) 70 | 71 | if "%1" == "qthelp" ( 72 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% _build/qthelp 73 | echo. 74 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 75 | .qhcp project file in _build/qthelp, like this: 76 | echo.^> qcollectiongenerator _build\qthelp\Yapsy.qhcp 77 | echo.To view the help file: 78 | echo.^> assistant -collectionFile _build\qthelp\Yapsy.ghc 79 | goto end 80 | ) 81 | 82 | if "%1" == "latex" ( 83 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex 84 | echo. 85 | echo.Build finished; the LaTeX files are in _build/latex. 86 | goto end 87 | ) 88 | 89 | if "%1" == "changes" ( 90 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes 91 | echo. 92 | echo.The overview file is in _build/changes. 93 | goto end 94 | ) 95 | 96 | if "%1" == "linkcheck" ( 97 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% _build/linkcheck 98 | echo. 99 | echo.Link check complete; look for any errors in the above output ^ 100 | or in _build/linkcheck/output.txt. 101 | goto end 102 | ) 103 | 104 | if "%1" == "doctest" ( 105 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% _build/doctest 106 | echo. 107 | echo.Testing of doctests in the sources finished, look at the ^ 108 | results in _build/doctest/output.txt. 109 | goto end 110 | ) 111 | 112 | :end 113 | -------------------------------------------------------------------------------- /package/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 3 | 4 | 5 | """ 6 | Main file to launch the tests. 7 | """ 8 | 9 | import sys 10 | import getopt 11 | import unittest 12 | import logging 13 | 14 | 15 | from test.test_All import MainTestSuite 16 | 17 | def usage(): 18 | """ 19 | Show/explain the options. 20 | """ 21 | return """python main.py [OPTIONS] 22 | 23 | Options: 24 | 25 | -h or --help Print this help text 26 | 27 | -d Switch the logger to DEBUG mode. 28 | 29 | -v Switch the test to verbose mode. 30 | """ 31 | 32 | 33 | def main(argv): 34 | """ 35 | Launch all the test. 36 | """ 37 | try: 38 | opts, args = getopt.getopt(argv[1:], "vdh", ["help"]) 39 | except getopt.GetoptError: 40 | print(usage()) 41 | sys.exit(2) 42 | loglevel = logging.ERROR 43 | test_verbosity = 1 44 | for o,a in opts: 45 | if o in ("-h","--help"): 46 | print(usage()) 47 | sys.exit(0) 48 | elif o == "-d": 49 | loglevel = logging.DEBUG 50 | elif o == "-v": 51 | test_verbosity = 2 52 | logging.basicConfig(level= loglevel, 53 | format='%(asctime)s %(levelname)s %(message)s') 54 | 55 | # launch the testing process 56 | unittest.TextTestRunner(verbosity=test_verbosity).run(MainTestSuite) 57 | 58 | 59 | 60 | if __name__=="__main__": 61 | main(sys.argv) 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /package/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*- 3 | 4 | """ 5 | The setup.py script needed to build a .egg for an easier distribution 6 | and installation of yapsy. 7 | 8 | Requires 'Easy Install' to be installed :) 9 | see there: http://peak.telecommunity.com/DevCenter/EasyInstall#installation-instructions 10 | 11 | Then to create a package run: 12 | $ python setup.py bdist_egg 13 | 14 | To use the generated .egg file then: 15 | easy_install Yapsy-{yapsy version}-py{python version}.egg 16 | 17 | Automagical stuff: 18 | 19 | - test everything:: 20 | 21 | python setup.py test 22 | 23 | - build the packages (sources an egg) and upload all the stuff to pypi:: 24 | 25 | python setup.py sdist bdist_egg upload 26 | 27 | - build the documentation 28 | 29 | python setup.py build_sphinx 30 | """ 31 | 32 | import os 33 | from setuptools import setup 34 | 35 | # just in case setup.py is launched from elsewhere that the containing directory 36 | originalDir = os.getcwd() 37 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 38 | try: 39 | setup( 40 | name = "Yapsy", 41 | version = __import__("yapsy").__version__, 42 | packages = ['yapsy'], 43 | package_dir = {'yapsy':'yapsy'}, 44 | 45 | # the unit tests 46 | test_suite = "test.test_All.MainTestSuite", 47 | 48 | # metadata for upload to PyPI 49 | author = "Thibauld Nion", 50 | author_email = "thibauld@tibonihoo.net", 51 | description = "Yet another plugin system", 52 | license = "BSD", 53 | keywords = "plugin manager", 54 | url = "http://yapsy.sourceforge.net", 55 | # more details 56 | long_description = open("README.txt").read(), 57 | classifiers=['Development Status :: 5 - Production/Stable', 58 | 'Intended Audience :: Developers', 59 | 'License :: OSI Approved :: BSD License', 60 | 'Operating System :: OS Independent', 61 | 'Programming Language :: Python', 62 | 'Programming Language :: Python :: 3', 63 | 'Topic :: Software Development :: Libraries :: Python Modules'], 64 | platforms='All', 65 | ) 66 | 67 | finally: 68 | os.chdir(originalDir) 69 | -------------------------------------------------------------------------------- /package/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | """ 3 | Gathers the unittests of yapsy 4 | """ 5 | -------------------------------------------------------------------------------- /package/test/plugins/ConfigPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from yapsy.IPlugin import IPlugin 9 | 10 | class ConfigPlugin(IPlugin): 11 | """ 12 | Try to use the methods with which it has been decorated. 13 | """ 14 | 15 | def __init__(self): 16 | """ 17 | init 18 | """ 19 | # initialise parent class 20 | IPlugin.__init__(self) 21 | 22 | 23 | def activate(self): 24 | """ 25 | Call the parent class's acivation method 26 | """ 27 | IPlugin.activate(self) 28 | return 29 | 30 | 31 | def deactivate(self): 32 | """ 33 | Just call the parent class's method 34 | """ 35 | IPlugin.deactivate(self) 36 | 37 | 38 | def choseTestOption(self, value): 39 | """ 40 | Set an option to a given value. 41 | """ 42 | self.setConfigOption("Test",value) 43 | 44 | def checkTestOption(self): 45 | """ 46 | Test if the test option is here. 47 | """ 48 | return self.hasConfigOption("Test") 49 | 50 | def getTestOption(self): 51 | """ 52 | Return the value of the test option. 53 | """ 54 | return self.getConfigOption("Test") 55 | 56 | 57 | -------------------------------------------------------------------------------- /package/test/plugins/ErroneousPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from yapsy.IPlugin import IPlugin 9 | 10 | from import_error import the_error_is_here 11 | 12 | class ErrorenousPlugin(IPlugin): 13 | """ 14 | Only trigger the expected test results. 15 | """ 16 | 17 | def __init__(self): 18 | """ 19 | init 20 | """ 21 | # initialise parent class 22 | IPlugin.__init__(self) 23 | 24 | 25 | def activate(self): 26 | """ 27 | On activation tell that this has been successfull. 28 | """ 29 | # get the automatic procedure from IPlugin 30 | IPlugin.activate(self) 31 | return 32 | 33 | 34 | def deactivate(self): 35 | """ 36 | On deactivation check that the 'activated' flag was on then 37 | tell everything's ok to the test procedure. 38 | """ 39 | IPlugin.deactivate(self) 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package/test/plugins/LegacyMultiprocessPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | A simple multiprocessed plugin that echoes the content received to the parent 5 | """ 6 | 7 | from yapsy.IMultiprocessChildPlugin import IMultiprocessChildPlugin 8 | 9 | class LegacyMultiprocessPlugin(IMultiprocessChildPlugin): 10 | """ 11 | Only trigger the expected test results. 12 | """ 13 | 14 | def __init__(self, parent_pipe): 15 | IMultiprocessChildPlugin.__init__(self, parent_pipe=parent_pipe) 16 | 17 | def run(self): 18 | content_from_parent = self.parent_pipe.recv() 19 | self.parent_pipe.send("{0}|echo_from_child".format(content_from_parent)) 20 | -------------------------------------------------------------------------------- /package/test/plugins/SimpleMultiprocessPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | A simple multiprocessed plugin that echoes the content received to the parent 5 | """ 6 | 7 | from yapsy.IMultiprocessPlugin import IMultiprocessPlugin 8 | 9 | class SimpleMultiprocessPlugin(IMultiprocessPlugin): 10 | """ 11 | Only trigger the expected test results. 12 | """ 13 | 14 | def __init__(self, parent_pipe): 15 | IMultiprocessPlugin.__init__(self, parent_pipe=parent_pipe) 16 | 17 | def run(self): 18 | content_from_parent = self.parent_pipe.recv() 19 | self.parent_pipe.send("{0}|echo_from_child".format(content_from_parent)) 20 | 21 | -------------------------------------------------------------------------------- /package/test/plugins/SimplePlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from yapsy.IPlugin import IPlugin 9 | 10 | class SimplePlugin(IPlugin): 11 | """ 12 | Only trigger the expected test results. 13 | """ 14 | 15 | def __init__(self): 16 | """ 17 | init 18 | """ 19 | # initialise parent class 20 | IPlugin.__init__(self) 21 | 22 | 23 | def activate(self): 24 | """ 25 | On activation tell that this has been successfull. 26 | """ 27 | # get the automatic procedure from IPlugin 28 | IPlugin.activate(self) 29 | return 30 | 31 | 32 | def deactivate(self): 33 | """ 34 | On deactivation check that the 'activated' flag was on then 35 | tell everything's ok to the test procedure. 36 | """ 37 | IPlugin.deactivate(self) 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package/test/plugins/VersionedPlugin10.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from test_settings import TEST_MESSAGE 9 | from yapsy.IPlugin import IPlugin 10 | 11 | class VersionedPlugin10(IPlugin): 12 | """ 13 | Only trigger the expected test results. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | init 19 | """ 20 | # initialise parent class 21 | IPlugin.__init__(self) 22 | TEST_MESSAGE("Version 1.0") 23 | 24 | def activate(self): 25 | """ 26 | On activation tell that this has been successfull. 27 | """ 28 | # get the automatic procedure from IPlugin 29 | IPlugin.activate(self) 30 | TEST_MESSAGE("Activated Version 1.0!") 31 | return 32 | 33 | 34 | def deactivate(self): 35 | """ 36 | On deactivation check that the 'activated' flag was on then 37 | tell everything's ok to the test procedure. 38 | """ 39 | IPlugin.deactivate(self) 40 | TEST_MESSAGE("Deactivated Version 1.0!") 41 | 42 | 43 | -------------------------------------------------------------------------------- /package/test/plugins/VersionedPlugin11.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from test_settings import TEST_MESSAGE 9 | from yapsy.IPlugin import IPlugin 10 | 11 | class VersionedPlugin11(IPlugin): 12 | """ 13 | Only trigger the expected test results. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | init 19 | """ 20 | # initialise parent class 21 | IPlugin.__init__(self) 22 | TEST_MESSAGE("Version 1.1") 23 | 24 | def activate(self): 25 | """ 26 | On activation tell that this has been successfull. 27 | """ 28 | # get the automatic procedure from IPlugin 29 | IPlugin.activate(self) 30 | return 31 | 32 | 33 | def deactivate(self): 34 | """ 35 | On deactivation check that the 'activated' flag was on then 36 | tell everything's ok to the test procedure. 37 | """ 38 | IPlugin.deactivate(self) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package/test/plugins/VersionedPlugin111.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from test_settings import TEST_MESSAGE 9 | from yapsy.IPlugin import IPlugin 10 | 11 | class VersionedPlugin111(IPlugin): 12 | """ 13 | Only trigger the expected test results. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | init 19 | """ 20 | # initialise parent class 21 | IPlugin.__init__(self) 22 | TEST_MESSAGE("Version 1.1.1") 23 | 24 | def activate(self): 25 | """ 26 | On activation tell that this has been successfull. 27 | """ 28 | # get the automatic procedure from IPlugin 29 | IPlugin.activate(self) 30 | return 31 | 32 | 33 | def deactivate(self): 34 | """ 35 | On deactivation check that the 'activated' flag was on then 36 | tell everything's ok to the test procedure. 37 | """ 38 | IPlugin.deactivate(self) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package/test/plugins/VersionedPlugin12.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from test_settings import TEST_MESSAGE 9 | from yapsy.IPlugin import IPlugin 10 | 11 | class VersionedPlugin12(IPlugin): 12 | """ 13 | Only trigger the expected test results. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | init 19 | """ 20 | # initialise parent class 21 | IPlugin.__init__(self) 22 | TEST_MESSAGE("Version 1.2") 23 | 24 | def activate(self): 25 | """ 26 | On activation tell that this has been successfull. 27 | """ 28 | # get the automatic procedure from IPlugin 29 | IPlugin.activate(self) 30 | TEST_MESSAGE("Activated Version 1.2!") 31 | return 32 | 33 | 34 | def deactivate(self): 35 | """ 36 | On deactivation check that the 'activated' flag was on then 37 | tell everything's ok to the test procedure. 38 | """ 39 | IPlugin.deactivate(self) 40 | TEST_MESSAGE("Deactivated Version 1.2!") 41 | -------------------------------------------------------------------------------- /package/test/plugins/VersionedPlugin12a1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from test_settings import TEST_MESSAGE 9 | from yapsy.IPlugin import IPlugin 10 | 11 | class VersionedPlugin12a1(IPlugin): 12 | """ 13 | Only trigger the expected test results. 14 | """ 15 | 16 | def __init__(self): 17 | """ 18 | init 19 | """ 20 | # initialise parent class 21 | IPlugin.__init__(self) 22 | TEST_MESSAGE("Version 1.2a1") 23 | 24 | def activate(self): 25 | """ 26 | On activation tell that this has been successfull. 27 | """ 28 | # get the automatic procedure from IPlugin 29 | IPlugin.activate(self) 30 | TEST_MESSAGE("Activated Version 1.2a1!") 31 | return 32 | 33 | 34 | def deactivate(self): 35 | """ 36 | On deactivation check that the 'activated' flag was on then 37 | tell everything's ok to the test procedure. 38 | """ 39 | IPlugin.deactivate(self) 40 | TEST_MESSAGE("Deactivated Version 1.2a1!") 41 | -------------------------------------------------------------------------------- /package/test/plugins/configplugin.yapsy-config-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Config Plugin 3 | Module = ConfigPlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin with configuration handling 10 | -------------------------------------------------------------------------------- /package/test/plugins/configplugin.yapsy-filter-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Config Plugin 3 | Module = ConfigPlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin with configuration handling 10 | -------------------------------------------------------------------------------- /package/test/plugins/erroneousplugin.yapsy-error-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Erroreous Plugin 3 | Module = ErroneousPlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://yapsy.sourceforge.net 9 | Description = A simple plugin trigger a syntax error for basic testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/legacymultiprocessplugin.multiprocess-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Legacy Multiprocess Plugin 3 | Module = LegacyMultiprocessPlugin 4 | 5 | [Documentation] 6 | Author = Pierre-Yves Langlois 7 | Version = 0.1 8 | Description = A minimal plugin to test multiprocessing 9 | Copyright = 2015 10 | -------------------------------------------------------------------------------- /package/test/plugins/simplemultiprocessplugin.multiprocess-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Simple Multiprocess Plugin 3 | Module = SimpleMultiprocessPlugin 4 | 5 | [Documentation] 6 | Author = Pierre-Yves Langlois 7 | Version = 0.1 8 | Description = A minimal plugin to test multiprocessing 9 | Copyright = 2015 10 | -------------------------------------------------------------------------------- /package/test/plugins/simpleplugin.yapsy-filter-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Simple Plugin 3 | Module = SimplePlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin usefull for basic testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/simpleplugin.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Simple Plugin 3 | Module = SimplePlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin usefull for basic testing 10 | Copyright = 2014 11 | -------------------------------------------------------------------------------- /package/test/plugins/versioned10.version-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Versioned Plugin 3 | Module = VersionedPlugin10 4 | 5 | [Documentation] 6 | Author = Rob McMullen 7 | Version = 1.0 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin for version testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/versioned11.version-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Versioned Plugin 3 | Module = VersionedPlugin11 4 | 5 | [Documentation] 6 | Author = Rob McMullen 7 | Version = 1.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin for version testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/versioned111.version-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Versioned Plugin 3 | Module = VersionedPlugin111 4 | 5 | [Documentation] 6 | Author = Rob McMullen 7 | Version = 1.1.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin for version testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/versioned12.version-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Versioned Plugin 3 | Module = VersionedPlugin12 4 | 5 | [Documentation] 6 | Author = Rob McMullen 7 | Version = 1.2 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin for version testing 10 | -------------------------------------------------------------------------------- /package/test/plugins/versioned12a1.version-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Versioned Plugin 3 | Module = VersionedPlugin12a1 4 | 5 | [Documentation] 6 | Author = Rob McMullen 7 | Version = 1.2a1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin for version testing 10 | -------------------------------------------------------------------------------- /package/test/pluginsasdirs/SimplePlugin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | This is certainly the second simplest plugin ever. 5 | """ 6 | 7 | from yapsy.IPlugin import IPlugin 8 | 9 | class SimplePlugin(IPlugin): 10 | """ 11 | Only trigger the expected test results. 12 | """ 13 | 14 | def __init__(self): 15 | """ 16 | init 17 | """ 18 | # initialise parent class 19 | IPlugin.__init__(self) 20 | 21 | 22 | def activate(self): 23 | """ 24 | On activation tell that this has been successfull. 25 | """ 26 | # get the automatic procedure from IPlugin 27 | IPlugin.activate(self) 28 | return 29 | 30 | 31 | def deactivate(self): 32 | """ 33 | On deactivation check that the 'activated' flag was on then 34 | tell everything's ok to the test procedure. 35 | """ 36 | IPlugin.deactivate(self) 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /package/test/pluginsasdirs/simpleplugin.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Simple Plugin 3 | Module = SimplePlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin usefull for basic testing 10 | -------------------------------------------------------------------------------- /package/test/pluginstoinstall/AutoInstallPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from yapsy.IPlugin import IPlugin 9 | 10 | class AutoInstallPlugin(IPlugin): 11 | """ 12 | Only trigger the expected test results. 13 | """ 14 | 15 | def __init__(self): 16 | """ 17 | init 18 | """ 19 | # initialise parent class 20 | IPlugin.__init__(self) 21 | 22 | 23 | def activate(self): 24 | """ 25 | On activation tell that this has been successfull. 26 | """ 27 | # get the automatic procedure from IPlugin 28 | IPlugin.activate(self) 29 | return 30 | 31 | 32 | def deactivate(self): 33 | """ 34 | On deactivation check that the 'activated' flag was on then 35 | tell everything's ok to the test procedure. 36 | """ 37 | IPlugin.deactivate(self) 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package/test/pluginstoinstall/autoinstallWRONGzipplugin.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/test/pluginstoinstall/autoinstallWRONGzipplugin.zip -------------------------------------------------------------------------------- /package/test/pluginstoinstall/autoinstallZIPplugin.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tibonihoo/yapsy/6b487b04affb19ab40adbbc87827668bea0abcee/package/test/pluginstoinstall/autoinstallZIPplugin.zip -------------------------------------------------------------------------------- /package/test/pluginstoinstall/autoinstalldirplugin.yapsy-autoinstall-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Auto Install Dir Plugin 3 | Module = autoinstalldirplugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin usefull for basic testing 10 | -------------------------------------------------------------------------------- /package/test/pluginstoinstall/autoinstalldirplugin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | This is certainly the second simplest plugin ever. 6 | """ 7 | 8 | from yapsy.IPlugin import IPlugin 9 | 10 | class AutoInstallDirPlugin(IPlugin): 11 | """ 12 | Only trigger the expected test results. 13 | """ 14 | 15 | def __init__(self): 16 | """ 17 | init 18 | """ 19 | # initialise parent class 20 | IPlugin.__init__(self) 21 | 22 | 23 | def activate(self): 24 | """ 25 | On activation tell that this has been successfull. 26 | """ 27 | # get the automatic procedure from IPlugin 28 | IPlugin.activate(self) 29 | return 30 | 31 | 32 | def deactivate(self): 33 | """ 34 | On deactivation check that the 'activated' flag was on then 35 | tell everything's ok to the test procedure. 36 | """ 37 | IPlugin.deactivate(self) 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package/test/pluginstoinstall/autoinstallplugin.yapsy-autoinstall-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Auto Install Plugin 3 | Module = AutoInstallPlugin 4 | 5 | [Documentation] 6 | Author = Thibauld Nion 7 | Version = 0.1 8 | Website = http://mathbench.sourceforge.net 9 | Description = A simple plugin usefull for basic testing 10 | -------------------------------------------------------------------------------- /package/test/test_All.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | The test suite that binds them all... 5 | """ 6 | 7 | import sys 8 | import os 9 | import unittest 10 | 11 | # set correct loading path for test files 12 | sys.path.append( 13 | os.path.dirname( 14 | os.path.abspath(__file__))) 15 | 16 | 17 | # load the tests 18 | from . import test_SimplePlugin 19 | from . import test_Singleton 20 | from . import test_ConfigPlugin 21 | from . import test_VersionedPlugin 22 | from . import test_AutoInstallPlugin 23 | from . import test_FilterPlugin 24 | from . import test_ErrorInPlugin 25 | from . import test_PluginFileLocator 26 | from . import test_PluginInfo 27 | from . import test_SimpleMultiprocessPlugin 28 | 29 | # add them to a common test suite 30 | MainTestSuite = unittest.TestSuite( 31 | [ # add the tests suites below 32 | test_SimplePlugin.suite, 33 | test_Singleton.suite, 34 | test_ConfigPlugin.suite, 35 | test_VersionedPlugin.suite, 36 | test_AutoInstallPlugin.suite, 37 | test_FilterPlugin.suite, 38 | test_ErrorInPlugin.suite, 39 | test_PluginFileLocator.suite, 40 | test_PluginInfo.suite, 41 | test_SimpleMultiprocessPlugin.suite, 42 | ]) 43 | 44 | -------------------------------------------------------------------------------- /package/test/test_AutoInstallPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | import unittest 5 | import sys 6 | import os 7 | import shutil 8 | 9 | from yapsy.AutoInstallPluginManager import AutoInstallPluginManager 10 | 11 | class AutoInstallTestsCase(unittest.TestCase): 12 | """ 13 | Test the correct installation and loading of a simple plugin. 14 | """ 15 | 16 | 17 | def setUp(self): 18 | """ 19 | init 20 | """ 21 | # create the plugin manager 22 | self.storing_dir = os.path.join( 23 | os.path.dirname(os.path.abspath(__file__)),"plugins") 24 | self.pluginManager = AutoInstallPluginManager( 25 | self.storing_dir, 26 | directories_list=[self.storing_dir], 27 | plugin_info_ext="yapsy-autoinstall-plugin") 28 | # load the plugins that may be found 29 | self.pluginManager.collectPlugins() 30 | # Will be used later 31 | self.plugin_info = None 32 | self.new_plugins_waiting_dir = os.path.join( 33 | os.path.dirname(os.path.abspath(__file__)),"pluginstoinstall") 34 | 35 | 36 | def tearDown(self): 37 | """ 38 | Clean the plugin installation directory. 39 | """ 40 | try: 41 | os.remove(os.path.join(self.pluginManager.plugins_places[0], 42 | "autoinstallplugin.yapsy-autoinstall-plugin")) 43 | except OSError: 44 | pass 45 | try: 46 | os.remove(os.path.join(self.pluginManager.plugins_places[0], 47 | "AutoInstallPlugin.py")) 48 | except OSError: 49 | pass 50 | try: 51 | os.remove(os.path.join(self.pluginManager.plugins_places[0], 52 | "autoinstalldirplugin.yapsy-autoinstall-plugin")) 53 | except OSError: 54 | pass 55 | try: 56 | shutil.rmtree(os.path.join(self.pluginManager.plugins_places[0], 57 | "autoinstalldirplugin")) 58 | except OSError: 59 | pass 60 | 61 | 62 | def plugin_loading_check_none(self): 63 | """ 64 | Test that no plugin has been loaded. 65 | """ 66 | # check nb of categories 67 | self.assertEqual(len(self.pluginManager.getCategories()),1) 68 | sole_category = self.pluginManager.getCategories()[0] 69 | # check the number of plugins 70 | self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),0) 71 | 72 | def plugin_loading_check(self,new_plugin_name): 73 | """ 74 | Test if the correct plugin has been loaded. 75 | """ 76 | if self.plugin_info is None: 77 | # check nb of categories 78 | self.assertEqual(len(self.pluginManager.getCategories()),1) 79 | sole_category = self.pluginManager.getCategories()[0] 80 | # check the number of plugins 81 | self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1) 82 | self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0] 83 | # test that the name of the plugin has been correctly defined 84 | self.assertEqual(self.plugin_info.name,new_plugin_name) 85 | self.assertEqual(sole_category,self.plugin_info.category) 86 | else: 87 | self.assertTrue(True) 88 | 89 | def testGetSetInstallDir(self): 90 | """ 91 | Test getting and setting install dir. 92 | """ 93 | self.assertEqual(self.storing_dir,self.pluginManager.getInstallDir()) 94 | custom_install_dir = os.path.join("mouf", "bla") 95 | self.pluginManager.setInstallDir(custom_install_dir) 96 | self.assertEqual(custom_install_dir, self.pluginManager.getInstallDir()) 97 | 98 | 99 | def testNoneLoaded(self): 100 | """ 101 | Test if the correct plugin has been loaded. 102 | """ 103 | self.plugin_loading_check_none() 104 | 105 | def testInstallFile(self): 106 | """ 107 | Test if the correct plugin (defined by a file) can be installed and loaded. 108 | """ 109 | install_success = self.pluginManager.install(self.new_plugins_waiting_dir, 110 | "autoinstallplugin.yapsy-autoinstall-plugin") 111 | self.assertTrue(install_success) 112 | self.pluginManager.collectPlugins() 113 | self.plugin_loading_check("Auto Install Plugin") 114 | 115 | 116 | def testInstallDir(self): 117 | """ 118 | Test if the correct plugin (define by a directory) can be installed and loaded. 119 | """ 120 | install_success = self.pluginManager.install(self.new_plugins_waiting_dir, 121 | "autoinstalldirplugin.yapsy-autoinstall-plugin") 122 | self.assertTrue(install_success) 123 | self.pluginManager.collectPlugins() 124 | self.plugin_loading_check("Auto Install Dir Plugin") 125 | 126 | 127 | def testActivationAndDeactivation(self): 128 | """ 129 | Test if the activation procedure works. 130 | """ 131 | install_success = self.pluginManager.install(self.new_plugins_waiting_dir, 132 | "autoinstallplugin.yapsy-autoinstall-plugin") 133 | self.assertTrue(install_success) 134 | self.pluginManager.collectPlugins() 135 | self.plugin_loading_check("Auto Install Plugin") 136 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 137 | self.pluginManager.activatePluginByName(self.plugin_info.name, 138 | self.plugin_info.category) 139 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 140 | self.pluginManager.deactivatePluginByName(self.plugin_info.name, 141 | self.plugin_info.category) 142 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 143 | 144 | class AutoInstallZIPTestsCase(unittest.TestCase): 145 | """ 146 | Test the correct installation and loading of a zipped plugin. 147 | """ 148 | 149 | 150 | def setUp(self): 151 | """ 152 | init 153 | """ 154 | # create the plugin manager 155 | storing_dir = os.path.join( 156 | os.path.dirname(os.path.abspath(__file__)),"plugins") 157 | self.pluginManager = AutoInstallPluginManager( 158 | storing_dir, 159 | directories_list=[storing_dir], 160 | plugin_info_ext="yapsy-autoinstall-plugin") 161 | # load the plugins that may be found 162 | self.pluginManager.collectPlugins() 163 | # Will be used later 164 | self.plugin_info = None 165 | self.new_plugins_waiting_dir = os.path.join( 166 | os.path.dirname(os.path.abspath(__file__)),"pluginstoinstall") 167 | 168 | 169 | def tearDown(self): 170 | """ 171 | Clean the plugin installation directory. 172 | """ 173 | try: 174 | os.remove(os.path.join(self.pluginManager.plugins_places[0], 175 | "autoinstallzipplugin.yapsy-autoinstall-plugin")) 176 | except OSError: 177 | pass 178 | try: 179 | shutil.rmtree(os.path.join(self.pluginManager.plugins_places[0], 180 | "autoinstallzipplugin")) 181 | except OSError: 182 | pass 183 | 184 | 185 | def plugin_loading_check_none(self): 186 | """ 187 | Test that no plugin has been loaded. 188 | """ 189 | # check nb of categories 190 | self.assertEqual(len(self.pluginManager.getCategories()),1) 191 | sole_category = self.pluginManager.getCategories()[0] 192 | # check the number of plugins 193 | self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),0) 194 | 195 | def plugin_loading_check(self,new_plugin_name): 196 | """ 197 | Test if the correct plugin has been loaded. 198 | """ 199 | if self.plugin_info is None: 200 | # check nb of categories 201 | self.assertEqual(len(self.pluginManager.getCategories()),1) 202 | sole_category = self.pluginManager.getCategories()[0] 203 | # check the number of plugins 204 | self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1) 205 | self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0] 206 | # test that the name of the plugin has been correctly defined 207 | self.assertEqual(self.plugin_info.name,new_plugin_name) 208 | self.assertEqual(sole_category,self.plugin_info.category) 209 | else: 210 | self.assertTrue(True) 211 | 212 | def testNoneLoaded(self): 213 | """ 214 | Test if the correct plugin has been loaded. 215 | """ 216 | self.plugin_loading_check_none() 217 | 218 | def testInstallZIP(self): 219 | """ 220 | Test if the correct plugin (define by a zip file) can be installed and loaded. 221 | """ 222 | test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallZIPplugin.zip") 223 | if sys.version_info < (2, 6): 224 | self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file) 225 | return 226 | install_success = self.pluginManager.installFromZIP(test_file) 227 | self.assertTrue(install_success) 228 | self.pluginManager.collectPlugins() 229 | self.plugin_loading_check("Auto Install ZIP Plugin") 230 | 231 | def testInstallZIPFailOnWrongZip(self): 232 | """ 233 | Test if, when the zip file does not contain what is required the installation fails. 234 | """ 235 | test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallWRONGzipplugin.zip") 236 | if sys.version_info < (2, 6): 237 | self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file) 238 | return 239 | install_success = self.pluginManager.installFromZIP(test_file) 240 | self.assertFalse(install_success) 241 | self.pluginManager.collectPlugins() 242 | self.plugin_loading_check_none() 243 | 244 | def testInstallZIPFailOnUnexistingFile(self): 245 | """ 246 | Test if, when the zip file is not a file. 247 | """ 248 | test_file = os.path.join(self.new_plugins_waiting_dir,"doesNotExists.zip") 249 | if sys.version_info < (2, 6): 250 | self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file) 251 | return 252 | install_success = self.pluginManager.installFromZIP(test_file) 253 | self.assertFalse(install_success) 254 | self.pluginManager.collectPlugins() 255 | self.plugin_loading_check_none() 256 | 257 | def testInstallZIPFailOnNotAZipFile(self): 258 | """ 259 | Test if, when the zip file is not a valid zip. 260 | """ 261 | test_file = os.path.join(self.new_plugins_waiting_dir,"AutoInstallPlugin.py") 262 | if sys.version_info < (2, 6): 263 | self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file) 264 | return 265 | install_success = self.pluginManager.installFromZIP(test_file) 266 | self.assertFalse(install_success) 267 | self.pluginManager.collectPlugins() 268 | self.plugin_loading_check_none() 269 | 270 | def testActivationAndDeactivation(self): 271 | """ 272 | Test if the activation procedure works. 273 | """ 274 | test_file = os.path.join(self.new_plugins_waiting_dir,"autoinstallZIPplugin.zip") 275 | if sys.version_info < (2, 6): 276 | self.assertRaises(NotImplementedError,self.pluginManager.installFromZIP,test_file) 277 | return 278 | install_success = self.pluginManager.installFromZIP(test_file) 279 | self.assertTrue(install_success) 280 | self.pluginManager.collectPlugins() 281 | self.plugin_loading_check("Auto Install ZIP Plugin") 282 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 283 | self.pluginManager.activatePluginByName(self.plugin_info.name, 284 | self.plugin_info.category) 285 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 286 | self.pluginManager.deactivatePluginByName(self.plugin_info.name, 287 | self.plugin_info.category) 288 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 289 | 290 | 291 | 292 | suite = unittest.TestSuite([ 293 | unittest.TestLoader().loadTestsFromTestCase(AutoInstallTestsCase), 294 | unittest.TestLoader().loadTestsFromTestCase(AutoInstallZIPTestsCase), 295 | ]) 296 | -------------------------------------------------------------------------------- /package/test/test_ConfigPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | 5 | import os 6 | import unittest 7 | 8 | from configparser import ConfigParser 9 | from yapsy.ConfigurablePluginManager import ConfigurablePluginManager 10 | 11 | 12 | class ConfigTestMixin: 13 | 14 | def plugin_loading_check(self): 15 | """ 16 | Test if the correct plugin has been loaded. 17 | """ 18 | if self.plugin_info is None: 19 | # check nb of categories 20 | self.assertEqual(len(self.pluginManager.getCategories()),1) 21 | sole_category = self.pluginManager.getCategories()[0] 22 | # check the number of plugins 23 | self.assertEqual(len(self.pluginManager.getPluginsOfCategory(sole_category)),1) 24 | self.plugin_info = self.pluginManager.getPluginsOfCategory(sole_category)[0] 25 | # test that the name of the plugin has been correctly defined 26 | self.assertEqual(self.plugin_info.name,"Config Plugin") 27 | self.assertEqual(sole_category,self.plugin_info.category) 28 | else: 29 | self.assertTrue(True) 30 | 31 | def plugin_activate(self): 32 | """ 33 | Activate the plugin with basic checking 34 | """ 35 | self.plugin_loading_check() 36 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 37 | self.pluginManager.activatePluginByName(self.plugin_info.name, 38 | self.plugin_info.category) 39 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 40 | 41 | 42 | class ConfigTestCase(unittest.TestCase, ConfigTestMixin): 43 | """ 44 | Test the correct loading of a plugin that uses a configuration 45 | file through a ConfigurablePluginManager as well as basic 46 | commands. 47 | """ 48 | 49 | CONFIG_FILE = test_settings.TEMP_CONFIG_FILE_NAME 50 | 51 | def setUp(self): 52 | """ 53 | init 54 | """ 55 | # create a config file 56 | self.config_file = self.CONFIG_FILE 57 | self.config_parser = ConfigParser() 58 | self.plugin_info = None 59 | # create the plugin manager 60 | self.pluginManager = ConfigurablePluginManager( 61 | directories_list=[os.path.join( 62 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 63 | plugin_info_ext="yapsy-config-plugin", 64 | configparser_instance=self.config_parser, 65 | config_change_trigger=self.update_config) 66 | # load the plugins that may be found 67 | self.pluginManager.collectPlugins() 68 | 69 | def tearDown(self): 70 | """ 71 | When the test has been performed erase the temp file. 72 | """ 73 | if os.path.isfile(self.config_file): 74 | os.remove(self.config_file) 75 | 76 | def testConfigurationFileExistence(self): 77 | """ 78 | Test if the configuration file has been properly written. 79 | """ 80 | # activate the only loaded plugin 81 | self.plugin_activate() 82 | # get rid of the plugin manager and create a new one 83 | del self.pluginManager 84 | del self.config_parser 85 | self.config_parser = ConfigParser() 86 | self.config_parser.read(self.config_file) 87 | self.assertTrue(self.config_parser.has_section("Plugin Management")) 88 | self.assertTrue(self.config_parser.has_option("Plugin Management", 89 | "default_plugins_to_load")) 90 | self.pluginManager = ConfigurablePluginManager( 91 | directories_list=[os.path.join( 92 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 93 | plugin_info_ext="yapsy-config-plugin", 94 | configparser_instance=self.config_parser, 95 | config_change_trigger=self.update_config) 96 | self.pluginManager.collectPlugins() 97 | self.plugin_loading_check() 98 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 99 | self.pluginManager.deactivatePluginByName(self.plugin_info.name, 100 | self.plugin_info.category) 101 | # check that activating the plugin once again, won't cause an error 102 | self.pluginManager.activatePluginByName(self.plugin_info.name, 103 | self.plugin_info.category) 104 | # Will be used later 105 | self.plugin_info = None 106 | 107 | def testLoaded(self): 108 | """ 109 | Test if the correct plugin has been loaded. 110 | """ 111 | self.plugin_loading_check() 112 | 113 | def testActivationAndDeactivation(self): 114 | """ 115 | Test if the activation/deactivaion procedures work. 116 | """ 117 | self.plugin_activate() 118 | self.pluginManager.deactivatePluginByName(self.plugin_info.name, 119 | self.plugin_info.category) 120 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 121 | 122 | def testPluginOptions(self): 123 | """ 124 | Test is the plugin can register and access options from the 125 | ConfigParser. 126 | """ 127 | self.plugin_activate() 128 | plugin = self.plugin_info.plugin_object 129 | plugin.choseTestOption("voila") 130 | self.assertTrue(plugin.checkTestOption()) 131 | self.assertEqual(plugin.getTestOption(),"voila") 132 | 133 | def update_config(self): 134 | """ 135 | Write the content of the ConfigParser in a file. 136 | """ 137 | cf = open(self.config_file,"a") 138 | self.config_parser.write(cf) 139 | cf.close() 140 | 141 | 142 | class ConfigurablePMWithDefaultChangeTriggerTestCase(unittest.TestCase, ConfigTestMixin): 143 | """Test the correctness of default values of args specific to the 144 | ConfigurablePM in its construtor. 145 | """ 146 | 147 | def setUp(self): 148 | """ 149 | init 150 | """ 151 | # create a config file 152 | self.config_parser = ConfigParser() 153 | self.plugin_info = None 154 | # create the plugin manager 155 | self.pluginManager = ConfigurablePluginManager( 156 | directories_list=[os.path.join( 157 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 158 | plugin_info_ext="yapsy-config-plugin", 159 | configparser_instance=self.config_parser) 160 | # load the plugins that may be found 161 | self.pluginManager.collectPlugins() 162 | 163 | def testPluginOptions(self): 164 | """ 165 | Test is the plugin can register and access options from the 166 | ConfigParser. 167 | """ 168 | self.plugin_activate() 169 | plugin = self.plugin_info.plugin_object 170 | plugin.choseTestOption("voila") 171 | self.assertTrue(plugin.checkTestOption()) 172 | self.assertEqual(plugin.getTestOption(),"voila") 173 | 174 | 175 | 176 | suite = unittest.TestSuite([ 177 | unittest.TestLoader().loadTestsFromTestCase(ConfigTestCase), 178 | unittest.TestLoader().loadTestsFromTestCase(ConfigurablePMWithDefaultChangeTriggerTestCase), 179 | ]) 180 | -------------------------------------------------------------------------------- /package/test/test_ErrorInPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | 5 | import os 6 | import unittest 7 | import logging 8 | 9 | from yapsy.PluginManager import PluginManager 10 | from yapsy import log 11 | 12 | class ErrorTestCase(unittest.TestCase): 13 | """ 14 | Test the handling of errors during plugin load. 15 | """ 16 | 17 | def testTwoStepsLoadWithError(self): 18 | """ 19 | Test loading the plugins in two steps in order to collect more 20 | deltailed informations and take care of an erroneous plugin. 21 | """ 22 | spm = PluginManager(directories_list=[ 23 | os.path.join( 24 | os.path.dirname(os.path.abspath(__file__)),"plugins") 25 | ], plugin_info_ext="yapsy-error-plugin") 26 | # trigger the first step to look up for plugins 27 | spm.locatePlugins() 28 | # make full use of the "feedback" the loadPlugins can give 29 | # - set-up the callback function that will be called *before* 30 | # loading each plugin 31 | callback_infos = [] 32 | def preload_cbk(i_plugin_info): 33 | callback_infos.append(i_plugin_info) 34 | callback_after_infos = [] 35 | def postload_cbk(i_plugin_info): 36 | callback_after_infos.append(i_plugin_info) 37 | # - gather infos about the processed plugins (loaded or not) 38 | # and for the test, monkey patch the logger 39 | originalLogLevel = log.getEffectiveLevel() 40 | log.setLevel(logging.ERROR) 41 | errorLogCallFlag = [False] 42 | def errorMock(*args,**kwargs): 43 | errorLogCallFlag[0]=True 44 | originalErrorMethod = log.error 45 | log.error = errorMock 46 | try: 47 | loadedPlugins = spm.loadPlugins(callback=preload_cbk, callback_after=postload_cbk) 48 | finally: 49 | log.setLevel(originalLogLevel) 50 | log.error = originalErrorMethod 51 | self.assertTrue(errorLogCallFlag[0]) 52 | self.assertEqual(len(loadedPlugins),1) 53 | self.assertEqual(len(callback_infos),1) 54 | self.assertTrue(isinstance(callback_infos[0].error,tuple)) 55 | self.assertEqual(loadedPlugins[0],callback_infos[0]) 56 | self.assertTrue(issubclass(callback_infos[0].error[0],ImportError)) 57 | self.assertEqual(len(callback_after_infos),0) 58 | # check that the getCategories works 59 | self.assertEqual(len(spm.getCategories()),1) 60 | sole_category = spm.getCategories()[0] 61 | # check the getPluginsOfCategory 62 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0) 63 | 64 | 65 | 66 | suite = unittest.TestSuite([ 67 | unittest.TestLoader().loadTestsFromTestCase(ErrorTestCase), 68 | ]) 69 | -------------------------------------------------------------------------------- /package/test/test_FilterPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | from .test_settings import TEST_MESSAGE 5 | import unittest 6 | import os 7 | import re 8 | 9 | from yapsy.FilteredPluginManager import FilteredPluginManager 10 | 11 | 12 | class testFilter(FilteredPluginManager): 13 | """ 14 | Test filter class. 15 | Refused to load plugins whose Name starts with 'C'. 16 | """ 17 | _bannednames = re.compile(r"^C") 18 | 19 | def isPluginOk(self,info): 20 | return not self._bannednames.match(info.name) 21 | 22 | 23 | class FilteredTestsCase(unittest.TestCase): 24 | """ 25 | Test the correct loading of a simple plugin as well as basic 26 | commands. 27 | """ 28 | 29 | def setUp(self): 30 | """ 31 | init 32 | """ 33 | # create the plugin manager 34 | # print os.path.join(os.path.dirname(os.path.abspath(__file__)),"plugins") 35 | self.filteredPluginManager = testFilter( 36 | directories_list=[os.path.join( 37 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 38 | plugin_info_ext="yapsy-filter-plugin", 39 | ) 40 | # load the plugins that may be found 41 | self.filteredPluginManager.collectPlugins() 42 | # Will be used later 43 | self.plugin_info = None 44 | 45 | def plugin_loading_check(self): 46 | """ 47 | Test if the correct plugins have been loaded. 48 | """ 49 | # check nb of categories 50 | self.assertEqual(len(self.filteredPluginManager.getCategories()),1) 51 | sole_category = self.filteredPluginManager.getCategories()[0] 52 | # check the number of plugins 53 | self.assertEqual(len(self.filteredPluginManager.getPluginsOfCategory(sole_category)),1) 54 | plugins = self.filteredPluginManager.getPluginsOfCategory(sole_category) 55 | for plugin_info in plugins: 56 | TEST_MESSAGE("plugin info: %s" % plugin_info) 57 | self.plugin_info = plugin_info 58 | self.assertTrue(self.plugin_info) 59 | self.assertEqual(self.plugin_info.name,"Simple Plugin") 60 | self.assertEqual(sole_category,self.plugin_info.category) 61 | 62 | def testLoaded(self): 63 | """ 64 | Test if the correct plugin has been loaded. 65 | """ 66 | self.plugin_loading_check() 67 | 68 | 69 | def testActivationAndDeactivation(self): 70 | """ 71 | Test if the activation procedure works. 72 | """ 73 | self.plugin_loading_check() 74 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 75 | TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object) 76 | self.plugin_info.plugin_object.activate() 77 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 78 | self.plugin_info.plugin_object.deactivate() 79 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 80 | 81 | 82 | def testRejectedList(self): 83 | """ 84 | Test if the list of rejected plugins is correct. 85 | """ 86 | for plugin in self.filteredPluginManager.getRejectedPlugins(): 87 | TEST_MESSAGE("plugin info: %s" % plugin[2]) 88 | self.assertEqual(plugin[2].name,"Config Plugin") 89 | 90 | def testRejectedStable(self): 91 | reject1 = list(self.filteredPluginManager.getRejectedPlugins()) 92 | self.filteredPluginManager.collectPlugins() 93 | reject2 = list(self.filteredPluginManager.getRejectedPlugins()) 94 | self.assertEqual(len(reject1),len(reject2)) 95 | 96 | 97 | def testRejectPlugin(self): 98 | self.filteredPluginManager.locatePlugins() 99 | rejected = self.filteredPluginManager.rejectedPlugins 100 | #If this fails the test in not meaningful.. 101 | self.assertTrue(len(rejected) > 0) 102 | nrRejected = len(rejected) 103 | for plugin in rejected: 104 | self.filteredPluginManager.rejectPluginCandidate(plugin) 105 | self.assertEqual(nrRejected,len(self.filteredPluginManager.rejectedPlugins)) 106 | 107 | def testRemovePlugin(self): 108 | self.filteredPluginManager.locatePlugins() 109 | rejected = self.filteredPluginManager.rejectedPlugins 110 | nrCandidates = len(self.filteredPluginManager.getPluginCandidates()) 111 | #If this fails the test in not meaningful.. 112 | self.assertTrue(len(rejected) > 0) 113 | for plugin in rejected: 114 | self.filteredPluginManager.removePluginCandidate(plugin) 115 | self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins)) 116 | self.assertEqual( nrCandidates , len(self.filteredPluginManager.getPluginCandidates())) 117 | 118 | def testAppendRejectedPlugin(self): 119 | self.filteredPluginManager.locatePlugins() 120 | rejected = self.filteredPluginManager.getRejectedPlugins() 121 | nrRejected = len(rejected) 122 | nrCandidates = len(self.filteredPluginManager.getPluginCandidates()) 123 | 124 | #If this fails the test in not meaningful.. 125 | self.assertTrue(len(rejected) > 0) 126 | #Remove the rejected plugins into out own list. 127 | for plugin in rejected: 128 | self.filteredPluginManager.removePluginCandidate(plugin) 129 | self.assertEqual(len(self.filteredPluginManager.getRejectedPlugins()),0) 130 | 131 | ##Now Actually test Append. 132 | for plugin in rejected: 133 | self.filteredPluginManager.appendPluginCandidate(plugin) 134 | self.assertEqual(nrRejected ,len(self.filteredPluginManager.rejectedPlugins)) 135 | self.assertEqual(nrCandidates , len(self.filteredPluginManager.getPluginCandidates())) 136 | 137 | def testAppendOkPlugins(self): 138 | self.filteredPluginManager.locatePlugins() 139 | rejected = self.filteredPluginManager.getRejectedPlugins() 140 | nrRejected = len(rejected) 141 | nrCandidates = len(self.filteredPluginManager.getPluginCandidates()) 142 | 143 | #If this fails the test in not meaningful.. 144 | self.assertTrue(len(rejected) > 0) 145 | #Remove the rejected plugins again. 146 | for plugin in rejected: 147 | self.filteredPluginManager.removePluginCandidate(plugin) 148 | self.assertEqual(len(self.filteredPluginManager.getRejectedPlugins()),0) 149 | 150 | for plugin in rejected: 151 | #change the name so it is acceptable. 152 | plugin[2].name = "X" + plugin[2].name[1:] 153 | self.filteredPluginManager.appendPluginCandidate(plugin) 154 | self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins)) 155 | self.assertEqual(nrRejected + nrCandidates , len(self.filteredPluginManager.getPluginCandidates())) 156 | 157 | 158 | 159 | 160 | def testUnrejectPlugin(self): 161 | self.filteredPluginManager.locatePlugins() 162 | rejected = self.filteredPluginManager.rejectedPlugins 163 | nrRejected = len(rejected) 164 | nrCandidates = len(self.filteredPluginManager.getPluginCandidates()) 165 | #If this fails the test in not meaningful.. 166 | self.assertTrue(len(rejected) > 0) 167 | for plugin in rejected: 168 | self.filteredPluginManager.unrejectPluginCandidate(plugin) 169 | self.assertEqual(0,len(self.filteredPluginManager.rejectedPlugins)) 170 | self.assertEqual( nrRejected + nrCandidates , 171 | len(self.filteredPluginManager.getPluginCandidates())) 172 | 173 | 174 | class FilteredWithMonkeyPathTestsCase(unittest.TestCase): 175 | """ 176 | Test the correct loading oand filtering of plugins when the FilteredPluginManager is just monkey-patched 177 | """ 178 | 179 | def setUp(self): 180 | """ 181 | init 182 | """ 183 | # create the plugin manager 184 | # print os.path.join(os.path.dirname(os.path.abspath(__file__)),"plugins") 185 | self.filteredPluginManager = FilteredPluginManager( 186 | directories_list=[os.path.join( 187 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 188 | plugin_info_ext="yapsy-filter-plugin", 189 | ) 190 | self.filteredPluginManager.isPluginOk = lambda info:not re.match("^C",info.name) 191 | # load the plugins that may be found 192 | self.filteredPluginManager.collectPlugins() 193 | # Will be used later 194 | self.plugin_info = None 195 | 196 | def plugin_loading_check(self): 197 | """ 198 | Test if the correct plugins have been loaded. 199 | """ 200 | # check nb of categories 201 | self.assertEqual(len(self.filteredPluginManager.getCategories()),1) 202 | sole_category = self.filteredPluginManager.getCategories()[0] 203 | # check the number of plugins 204 | self.assertEqual(len(self.filteredPluginManager.getPluginsOfCategory(sole_category)),1) 205 | plugins = self.filteredPluginManager.getPluginsOfCategory(sole_category) 206 | for plugin_info in plugins: 207 | TEST_MESSAGE("plugin info: %s" % plugin_info) 208 | self.plugin_info = plugin_info 209 | self.assertTrue(self.plugin_info) 210 | self.assertEqual(self.plugin_info.name,"Simple Plugin") 211 | self.assertEqual(sole_category,self.plugin_info.category) 212 | 213 | def testLoaded(self): 214 | """ 215 | Test if the correct plugin has been loaded. 216 | """ 217 | self.plugin_loading_check() 218 | 219 | 220 | def testActivationAndDeactivation(self): 221 | """ 222 | Test if the activation procedure works. 223 | """ 224 | self.plugin_loading_check() 225 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 226 | TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object) 227 | self.plugin_info.plugin_object.activate() 228 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 229 | self.plugin_info.plugin_object.deactivate() 230 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 231 | 232 | 233 | def testRejectedList(self): 234 | """ 235 | Test if the list of rejected plugins is correct. 236 | """ 237 | for plugin in self.filteredPluginManager.getRejectedPlugins(): 238 | TEST_MESSAGE("plugin info: %s" % plugin[2]) 239 | self.assertEqual(plugin[2].name,"Config Plugin") 240 | 241 | suite = unittest.TestSuite([ 242 | unittest.TestLoader().loadTestsFromTestCase(FilteredTestsCase), 243 | unittest.TestLoader().loadTestsFromTestCase(FilteredWithMonkeyPathTestsCase), 244 | ]) 245 | -------------------------------------------------------------------------------- /package/test/test_PluginInfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | import test_settings 4 | from configparser import ConfigParser 5 | import unittest 6 | 7 | 8 | from yapsy.PluginInfo import PluginInfo 9 | from packaging.version import Version 10 | 11 | 12 | class PluginInfoTest(unittest.TestCase): 13 | """ 14 | Test basic manipulations of PluginInfo. 15 | """ 16 | 17 | def testDefaultValuesAndAccessors(self): 18 | pi = PluginInfo("mouf","/bla/mouf") 19 | self.assertEqual("mouf",pi.name) 20 | self.assertEqual("/bla/mouf",pi.path) 21 | self.assertEqual(None,pi.plugin_object) 22 | self.assertEqual([],pi.categories) 23 | self.assertEqual(None,pi.error) 24 | self.assertEqual(Version("0.0"),pi.version) 25 | self.assertEqual("Unknown",pi.author) 26 | self.assertEqual("Unknown",pi.copyright) 27 | self.assertEqual("None",pi.website) 28 | self.assertEqual("",pi.description) 29 | self.assertEqual("UnknownCategory",pi.category) 30 | 31 | def testDetailsAccessors(self): 32 | pi = PluginInfo("mouf","/bla/mouf") 33 | details = ConfigParser() 34 | details.add_section("Core") 35 | details.set("Core","Name","hop") 36 | details.set("Core","Module","/greuh") 37 | details.add_section("Documentation") 38 | details.set("Documentation","Author","me") 39 | pi.details = details 40 | # Beware this is not so obvious: the plugin info still points 41 | # (and possibly modifies) the same instance of ConfigParser 42 | self.assertEqual(details,pi.details) 43 | # also the name and path are kept to their original value when 44 | # the details is set in one go. 45 | self.assertEqual("mouf",pi.name) 46 | self.assertEqual("/bla/mouf",pi.path) 47 | # check that some other info do change... 48 | self.assertEqual("me",pi.author) 49 | 50 | 51 | suite = unittest.TestSuite([ 52 | unittest.TestLoader().loadTestsFromTestCase(PluginInfoTest), 53 | ]) 54 | -------------------------------------------------------------------------------- /package/test/test_SimpleMultiprocessPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | import unittest 4 | import os 5 | 6 | from yapsy.MultiprocessPluginManager import MultiprocessPluginManager 7 | 8 | class SimpleMultiprocessTestCase(unittest.TestCase): 9 | """ 10 | Test the correct loading of a multiprocessed plugin as well as basic 11 | communication. 12 | """ 13 | 14 | def setUp(self): 15 | """ 16 | init 17 | """ 18 | # create the plugin manager 19 | self.mpPluginManager = MultiprocessPluginManager(directories_list=[ 20 | os.path.join( 21 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 22 | plugin_info_ext="multiprocess-plugin") 23 | # load the plugins that may be found 24 | self.mpPluginManager.collectPlugins() 25 | # Will be used later 26 | self.plugin_info = None 27 | 28 | def testUpAndRunning(self): 29 | """ 30 | Test if the plugin is loaded and if the communication pipe is properly setuped. 31 | """ 32 | for plugin_index, plugin in enumerate(self.mpPluginManager.getAllPlugins()): 33 | child_pipe = plugin.plugin_object.child_pipe 34 | content_from_parent = "hello-{0}-from-parent".format(plugin_index) 35 | child_pipe.send(content_from_parent) 36 | content_from_child = False 37 | if child_pipe.poll(5): 38 | content_from_child = child_pipe.recv() 39 | self.assertEqual("{0}|echo_from_child".format(content_from_parent), 40 | content_from_child) 41 | num_tested_plugin = plugin_index+1 42 | self.assertEqual(2, num_tested_plugin) 43 | 44 | suite = unittest.TestSuite([ 45 | unittest.TestLoader().loadTestsFromTestCase(SimpleMultiprocessTestCase), 46 | ]) 47 | -------------------------------------------------------------------------------- /package/test/test_SimplePlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | import unittest 5 | import os 6 | 7 | from yapsy.PluginManager import PluginManager 8 | from yapsy.IPlugin import IPlugin 9 | from yapsy.PluginFileLocator import PluginFileLocator 10 | from yapsy.PluginFileLocator import IPluginFileAnalyzer 11 | from yapsy import NormalizePluginNameForModuleName 12 | from configparser import ConfigParser 13 | 14 | class YapsyUtils(unittest.TestCase): 15 | 16 | def test_NormalizePluginNameForModuleName_on_ok_name(self): 17 | self.assertEqual("moufGlop2",NormalizePluginNameForModuleName("moufGlop2")) 18 | 19 | def test_NormalizePluginNameForModuleName_on_empty_name(self): 20 | self.assertEqual("_",NormalizePluginNameForModuleName("")) 21 | 22 | def test_NormalizePluginNameForModuleName_on_name_with_space(self): 23 | self.assertEqual("mouf_glop",NormalizePluginNameForModuleName("mouf glop")) 24 | 25 | def test_NormalizePluginNameForModuleName_on_name_with_nonalphanum(self): 26 | self.assertEqual("mouf__glop_a_é",NormalizePluginNameForModuleName("mouf+?glop:a/é")) 27 | 28 | 29 | 30 | class SimpleTestCase(unittest.TestCase): 31 | """ 32 | Test the correct loading of a simple plugin as well as basic 33 | commands. 34 | """ 35 | 36 | def setUp(self): 37 | """ 38 | init 39 | """ 40 | # create the plugin manager 41 | self.simplePluginManager = PluginManager(directories_list=[ 42 | os.path.join( 43 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 44 | # load the plugins that may be found 45 | self.simplePluginManager.collectPlugins() 46 | # Will be used later 47 | self.plugin_info = None 48 | 49 | def plugin_loading_check(self): 50 | """ 51 | Test if the correct plugin has been loaded. 52 | """ 53 | if self.plugin_info is None: 54 | # check nb of categories 55 | self.assertEqual(len(self.simplePluginManager.getCategories()),1) 56 | sole_category = self.simplePluginManager.getCategories()[0] 57 | # check the number of plugins 58 | self.assertEqual(len(self.simplePluginManager.getPluginsOfCategory(sole_category)),1) 59 | self.plugin_info = self.simplePluginManager.getPluginsOfCategory(sole_category)[0] 60 | # test that the name of the plugin has been correctly defined 61 | self.assertEqual(self.plugin_info.name,"Simple Plugin") 62 | self.assertEqual(sole_category,self.plugin_info.category) 63 | else: 64 | self.assertTrue(True) 65 | 66 | def testLoaded(self): 67 | """ 68 | Test if the correct plugin has been loaded. 69 | """ 70 | self.plugin_loading_check() 71 | 72 | def testGetAll(self): 73 | """ 74 | Test if the correct plugin has been loaded. 75 | """ 76 | self.plugin_loading_check() 77 | self.assertEqual(len(self.simplePluginManager.getAllPlugins()),1) 78 | self.assertEqual(self.simplePluginManager.getAllPlugins()[0],self.plugin_info) 79 | 80 | 81 | def testActivationAndDeactivation(self): 82 | """ 83 | Test if the activation procedure works. 84 | """ 85 | self.plugin_loading_check() 86 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 87 | self.simplePluginManager.activatePluginByName(self.plugin_info.name, 88 | self.plugin_info.category) 89 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 90 | self.simplePluginManager.deactivatePluginByName(self.plugin_info.name, 91 | self.plugin_info.category) 92 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 93 | 94 | 95 | class SimplePluginAdvancedManipulationTestsCase(unittest.TestCase): 96 | """ 97 | Test some advanced manipulation on the core data of a PluginManager. 98 | """ 99 | 100 | 101 | def testCategoryManipulation(self): 102 | """ 103 | Test querying, removing and adding plugins from/to a category. 104 | """ 105 | spm = PluginManager(directories_list=[ 106 | os.path.join( 107 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 108 | # load the plugins that may be found 109 | spm.collectPlugins() 110 | # check that the getCategories works 111 | self.assertEqual(len(spm.getCategories()),1) 112 | sole_category = spm.getCategories()[0] 113 | # check the getPluginsOfCategory 114 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1) 115 | plugin_info = spm.getPluginsOfCategory(sole_category)[0] 116 | # try to remove it and check that is worked 117 | spm.removePluginFromCategory(plugin_info,sole_category) 118 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0) 119 | # now re-add this plugin the to same category 120 | spm.appendPluginToCategory(plugin_info,sole_category) 121 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1) 122 | 123 | 124 | def testChangingCategoriesFilter(self): 125 | """ 126 | Test the effect of setting a new category filer. 127 | """ 128 | spm = PluginManager(directories_list=[ 129 | os.path.join( 130 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 131 | # load the plugins that may be found 132 | spm.collectPlugins() 133 | newCategory = "Mouf" 134 | # Pre-requisite for the test 135 | previousCategories = spm.getCategories() 136 | self.assertTrue(len(previousCategories) >= 1) 137 | self.assertTrue(newCategory not in previousCategories) 138 | # change the category and see what's happening 139 | spm.setCategoriesFilter({newCategory: IPlugin}) 140 | spm.collectPlugins() 141 | for categoryName in previousCategories: 142 | self.assertRaises(KeyError, spm.getPluginsOfCategory, categoryName) 143 | self.assertTrue(len(spm.getPluginsOfCategory(newCategory)) >= 1) 144 | 145 | 146 | def testCandidatesManipulation(self): 147 | """ 148 | Test querying, removing and adding plugins from/to the lkist 149 | of plugins to load. 150 | """ 151 | spm = PluginManager(directories_list=[ 152 | os.path.join( 153 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 154 | # locate the plugins that should be loaded 155 | spm.locatePlugins() 156 | # check nb of candidatesx 157 | self.assertEqual(len(spm.getPluginCandidates()),1) 158 | # get the description of the plugin candidate 159 | candidate = spm.getPluginCandidates()[0] 160 | self.assertTrue(isinstance(candidate,tuple)) 161 | # try removing the candidate 162 | spm.removePluginCandidate(candidate) 163 | self.assertEqual(len(spm.getPluginCandidates()),0) 164 | # try re-adding it 165 | spm.appendPluginCandidate(candidate) 166 | self.assertEqual(len(spm.getPluginCandidates()),1) 167 | 168 | def testTwoStepsLoad(self): 169 | """ 170 | Test loading the plugins in two steps in order to collect more 171 | deltailed informations. 172 | """ 173 | spm = PluginManager(directories_list=[ 174 | os.path.join( 175 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 176 | # trigger the first step to look up for plugins 177 | spm.locatePlugins() 178 | # make full use of the "feedback" the loadPlugins can give 179 | # - set-up the callback function that will be called *before* 180 | # loading each plugin 181 | callback_infos = [] 182 | def preload_cbk(plugin_info): 183 | callback_infos.append(plugin_info) 184 | callback_after_infos = [] 185 | def postload_cbk(plugin_info): 186 | callback_after_infos.append(plugin_info) 187 | # - gather infos about the processed plugins (loaded or not) 188 | loadedPlugins = spm.loadPlugins(callback=preload_cbk, callback_after=postload_cbk) 189 | self.assertEqual(len(loadedPlugins),1) 190 | self.assertEqual(len(callback_infos),1) 191 | self.assertEqual(loadedPlugins[0].error,None) 192 | self.assertEqual(loadedPlugins[0],callback_infos[0]) 193 | self.assertEqual(len(callback_after_infos),1) 194 | self.assertEqual(loadedPlugins[0],callback_infos[0]) 195 | # check that the getCategories works 196 | self.assertEqual(len(spm.getCategories()),1) 197 | sole_category = spm.getCategories()[0] 198 | # check the getPluginsOfCategory 199 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1) 200 | plugin_info = spm.getPluginsOfCategory(sole_category)[0] 201 | # try to remove it and check that is worked 202 | spm.removePluginFromCategory(plugin_info,sole_category) 203 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0) 204 | # now re-add this plugin the to same category 205 | spm.appendPluginToCategory(plugin_info,sole_category) 206 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1) 207 | 208 | def testMultipleCategoriesForASamePlugin(self): 209 | """ 210 | Test that associating a plugin to multiple categories works as expected. 211 | """ 212 | class AnotherPluginIfce(object): 213 | def __init__(self): 214 | pass 215 | def activate(self): 216 | pass 217 | def deactivate(self): 218 | pass 219 | 220 | spm = PluginManager( 221 | categories_filter = { 222 | "Default": IPlugin, 223 | "IP": IPlugin, 224 | "Other": AnotherPluginIfce, 225 | }, 226 | directories_list=[ 227 | os.path.join( 228 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 229 | # load the plugins that may be found 230 | spm.collectPlugins() 231 | # check that the getCategories works 232 | self.assertEqual(len(spm.getCategories()),3) 233 | categories = spm.getCategories() 234 | self.assertTrue("Default" in categories) 235 | # check the getPluginsOfCategory 236 | self.assertEqual(len(spm.getPluginsOfCategory("Default")), 1) 237 | plugin_info = spm.getPluginsOfCategory("Default")[0] 238 | self.assertTrue("Default" in plugin_info.categories) 239 | self.assertTrue("IP" in plugin_info.categories) 240 | self.assertTrue("IP" in categories) 241 | # check the getPluginsOfCategory 242 | self.assertEqual(len(spm.getPluginsOfCategory("IP")),1) 243 | self.assertTrue("Other" in categories) 244 | # check the getPluginsOfCategory 245 | self.assertEqual(len(spm.getPluginsOfCategory("Other")),0) 246 | # try to remove the plugin from one category and check the 247 | # other category 248 | spm.removePluginFromCategory(plugin_info, "Default") 249 | self.assertEqual(len(spm.getPluginsOfCategory("Default")), 0) 250 | self.assertEqual(len(spm.getPluginsOfCategory("IP")), 1) 251 | # now re-add this plugin the to same category 252 | spm.appendPluginToCategory(plugin_info, "Default") 253 | self.assertEqual(len(spm.getPluginsOfCategory("Default")),1) 254 | self.assertEqual(len(spm.getPluginsOfCategory("IP")),1) 255 | 256 | def testGetPluginOf(self): 257 | """ 258 | Test the plugin query function. 259 | """ 260 | spm = PluginManager( 261 | categories_filter = { 262 | "Default": IPlugin, 263 | "IP": IPlugin, 264 | }, 265 | directories_list=[ 266 | os.path.join( 267 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 268 | # load the plugins that may be found 269 | spm.collectPlugins() 270 | # check the getPluginsOfCategory 271 | self.assertEqual(len(spm.getPluginsOf(categories="IP")), 1) 272 | self.assertEqual(len(spm.getPluginsOf(categories="Default")), 1) 273 | self.assertEqual(len(spm.getPluginsOf(name="Simple Plugin")), 1) 274 | self.assertEqual(len(spm.getPluginsOf(is_activated=False)), 1) 275 | self.assertEqual(len(spm.getPluginsOf(categories="IP", is_activated=True)), 0) 276 | self.assertEqual(len(spm.getPluginsOf(categories="IP", is_activated=False)), 1) 277 | self.assertEqual(len(spm.getPluginsOf(categories="IP", pouet=False)), 0) 278 | self.assertEqual(len(spm.getPluginsOf(categories=["IP"])), 0) 279 | # The order in the categories are added to plugin info is random in this setup, hence the strange formula below 280 | self.assertEqual(len(spm.getPluginsOf(categories=["IP", "Default"]) | spm.getPluginsOf(categories=["Default", "IP"])), 1) 281 | self.assertEqual(len(spm.getPluginsOf(category="Default") | spm.getPluginsOf(category="IP")), 1) 282 | 283 | class SimplePluginDetectionTestsCase(unittest.TestCase): 284 | """ 285 | Test particular aspects of plugin detection 286 | """ 287 | 288 | def testRecursivePluginlocation(self): 289 | """ 290 | Test detection of plugins which by default must be 291 | recursive. Here we give the test directory as a plugin place 292 | whereas we expect the plugins to be in test/plugins. 293 | """ 294 | spm = PluginManager(directories_list=[ 295 | os.path.dirname(os.path.abspath(__file__))]) 296 | # load the plugins that may be found 297 | spm.collectPlugins() 298 | # check that the getCategories works 299 | self.assertEqual(len(spm.getCategories()),1) 300 | sole_category = spm.getCategories()[0] 301 | # check the getPluginsOfCategory 302 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),2) 303 | 304 | def testDisablingRecursivePluginLocationIsEnforced(self): 305 | """ 306 | Test detection of plugins when the detection is non recursive. 307 | Here we test that it cannot look into subdirectories of the 308 | test directory. 309 | """ 310 | pluginLocator = PluginFileLocator() 311 | pluginLocator.setPluginPlaces([ 312 | os.path.dirname(os.path.abspath(__file__))]) 313 | pluginLocator.disableRecursiveScan() 314 | spm = PluginManager() 315 | spm.setPluginLocator(pluginLocator) 316 | # load the plugins that may be found 317 | spm.collectPlugins() 318 | # check that the getCategories works 319 | self.assertEqual(len(spm.getCategories()),1) 320 | sole_category = spm.getCategories()[0] 321 | # check the getPluginsOfCategory 322 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),0) 323 | 324 | 325 | def testDisablingRecursivePluginLocationAllowsFindingTopLevelPlugins(self): 326 | """ 327 | Test detection of plugins when the detection is non 328 | recursive. Here we test that if we give test/plugin as the 329 | directory to scan it can find the plugin. 330 | """ 331 | pluginLocator = PluginFileLocator() 332 | pluginLocator.setPluginPlaces([ 333 | os.path.join( 334 | os.path.dirname(os.path.abspath(__file__)),"plugins")]) 335 | pluginLocator.disableRecursiveScan() 336 | spm = PluginManager() 337 | spm.setPluginLocator(pluginLocator) 338 | # load the plugins that may be found 339 | spm.collectPlugins() 340 | # check that the getCategories works 341 | self.assertEqual(len(spm.getCategories()),1) 342 | sole_category = spm.getCategories()[0] 343 | # check the getPluginsOfCategory 344 | self.assertEqual(len(spm.getPluginsOfCategory(sole_category)),1) 345 | 346 | def testEnforcingPluginDirsDoesNotKeepDefaultDir(self): 347 | """ 348 | Test that providing the directories list override the default search directory 349 | instead of extending the default list. 350 | """ 351 | 352 | class AcceptAllPluginFileAnalyzer(IPluginFileAnalyzer): 353 | 354 | def __init__(self): 355 | IPluginFileAnalyzer.__init__(self, "AcceptAll") 356 | 357 | def isValidPlugin(self, filename): 358 | return True 359 | 360 | def getInfosDictFromPlugin(self, dirpath, filename): 361 | return { "name": filename, "path": dirpath}, ConfigParser() 362 | 363 | pluginLocator = PluginFileLocator() 364 | pluginLocator.setAnalyzers([AcceptAllPluginFileAnalyzer()]) 365 | 366 | spm_default_dirs = PluginManager(plugin_locator= pluginLocator) 367 | spm_default_dirs.locatePlugins() 368 | candidates_in_default_dir = spm_default_dirs.getPluginCandidates() 369 | candidates_files_in_default_dir = set([c[0] for c in candidates_in_default_dir]) 370 | 371 | pluginLocator = PluginFileLocator() 372 | pluginLocator.setAnalyzers([AcceptAllPluginFileAnalyzer()]) 373 | spm = PluginManager(plugin_locator= pluginLocator, 374 | directories_list=[os.path.dirname(os.path.abspath(__file__)),"does-not-exists"]) 375 | spm.locatePlugins() 376 | candidates = spm.getPluginCandidates() 377 | candidates_files = set([c[0] for c in candidates]) 378 | 379 | self.assertFalse(set(candidates_files_in_default_dir).issubset(set(candidates_files))) 380 | 381 | suite = unittest.TestSuite([ 382 | unittest.TestLoader().loadTestsFromTestCase(YapsyUtils), 383 | unittest.TestLoader().loadTestsFromTestCase(SimpleTestCase), 384 | unittest.TestLoader().loadTestsFromTestCase(SimplePluginAdvancedManipulationTestsCase), 385 | unittest.TestLoader().loadTestsFromTestCase(SimplePluginDetectionTestsCase), 386 | ]) 387 | -------------------------------------------------------------------------------- /package/test/test_Singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | 5 | import os 6 | import unittest 7 | 8 | from yapsy.ConfigurablePluginManager import ConfigurablePluginManager 9 | from yapsy.VersionedPluginManager import VersionedPluginManager 10 | from yapsy.PluginManager import PluginManagerSingleton 11 | from configparser import ConfigParser 12 | 13 | 14 | """ 15 | There can be only one series of tests for the singleton, guess why ... 16 | """ 17 | 18 | class ConfigSingletonTestsCase(unittest.TestCase): 19 | """ 20 | Test the correct loading of a simple plugin as well as basic 21 | commands, use the Singleton version of the ConfigurablePluginManager. 22 | """ 23 | 24 | CONFIG_FILE = test_settings.TEMP_CONFIG_FILE_NAME 25 | 26 | def setUp(self): 27 | """ 28 | init 29 | """ 30 | # create a config file 31 | self.config_file = self.CONFIG_FILE 32 | self.config_parser = ConfigParser() 33 | self.plugin_info = None 34 | 35 | # create the plugin manager 36 | PluginManagerSingleton.setBehaviour([ConfigurablePluginManager,VersionedPluginManager]) 37 | pluginManager = PluginManagerSingleton.get() 38 | pluginManager.setPluginPlaces(directories_list=[os.path.dirname(os.path.abspath(__file__))]) 39 | pluginManager.setPluginInfoExtension("yapsy-config-plugin") 40 | pluginManager.setConfigParser(self.config_parser,self.update_config) 41 | # load the plugins that may be found 42 | pluginManager.collectPlugins() 43 | 44 | def tearDown(self): 45 | """ 46 | When the test has been performed erase the temp file. 47 | """ 48 | if os.path.isfile(self.config_file): 49 | os.remove(self.config_file) 50 | 51 | 52 | def testConfigurationFileExistence(self): 53 | """ 54 | Test if the configuration file has been properly written. 55 | """ 56 | # activate the only loaded plugin 57 | self.plugin_activate() 58 | # get rid of the plugin manager and create a new one 59 | self.config_parser.read(self.config_file) 60 | self.assertTrue(self.config_parser.has_section("Plugin Management")) 61 | self.assertTrue(self.config_parser.has_option("Plugin Management", 62 | "default_plugins_to_load")) 63 | 64 | 65 | def testLoaded(self): 66 | """ 67 | Test if the correct plugin has been loaded. 68 | """ 69 | self.plugin_loading_check() 70 | 71 | def testActivationAndDeactivation(self): 72 | """ 73 | Test if the activation/deactivaion procedures work. 74 | """ 75 | self.plugin_activate() 76 | PluginManagerSingleton.get().deactivatePluginByName(self.plugin_info.name, 77 | self.plugin_info.category) 78 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 79 | 80 | def testPluginOptions(self): 81 | """ 82 | Test is the plugin can register and access options from the 83 | ConfigParser. 84 | """ 85 | self.plugin_activate() 86 | plugin = self.plugin_info.plugin_object 87 | plugin.choseTestOption("voila") 88 | self.assertTrue(plugin.checkTestOption()) 89 | self.assertEqual(plugin.getTestOption(),"voila") 90 | 91 | 92 | #--- UTILITIES 93 | 94 | def plugin_loading_check(self): 95 | """ 96 | Test if the correct plugin has been loaded. 97 | """ 98 | if self.plugin_info is None: 99 | pluginManager = PluginManagerSingleton.get() 100 | # check nb of categories 101 | self.assertEqual(len(pluginManager.getCategories()),1) 102 | sole_category = pluginManager.getCategories()[0] 103 | # check the number of plugins 104 | self.assertEqual(len(pluginManager.getPluginsOfCategory(sole_category)),1) 105 | self.plugin_info = pluginManager.getPluginsOfCategory(sole_category)[0] 106 | # test that the name of the plugin has been correctly defined 107 | self.assertEqual(self.plugin_info.name,"Config Plugin") 108 | self.assertEqual(sole_category,self.plugin_info.category) 109 | else: 110 | self.assertTrue(True) 111 | 112 | def plugin_activate(self): 113 | """ 114 | Activate the plugin with basic checking 115 | """ 116 | self.plugin_loading_check() 117 | if not self.plugin_info.plugin_object.is_activated: 118 | PluginManagerSingleton.get().activatePluginByName(self.plugin_info.name, 119 | self.plugin_info.category) 120 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 121 | 122 | 123 | def update_config(self): 124 | """ 125 | Write the content of the ConfigParser in a file. 126 | """ 127 | cf = open(self.config_file,"a") 128 | self.config_parser.write(cf) 129 | cf.close() 130 | 131 | 132 | 133 | suite = unittest.TestSuite([ 134 | unittest.TestLoader().loadTestsFromTestCase(ConfigSingletonTestsCase), 135 | ]) 136 | -------------------------------------------------------------------------------- /package/test/test_VersionedPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | from . import test_settings 4 | from .test_settings import TEST_MESSAGE 5 | import unittest 6 | import os 7 | 8 | from yapsy.IPlugin import IPlugin 9 | from yapsy.VersionedPluginManager import VersionedPluginManager 10 | 11 | 12 | class VersionedTestsCase(unittest.TestCase): 13 | """ 14 | Test the correct loading of a simple plugin as well as basic 15 | commands. 16 | """ 17 | 18 | def setUp(self): 19 | """ 20 | init 21 | """ 22 | # create the plugin manager 23 | self.versionedPluginManager = VersionedPluginManager( 24 | directories_list=[os.path.join( 25 | os.path.dirname(os.path.abspath(__file__)),"plugins")], 26 | plugin_info_ext="version-plugin", 27 | ) 28 | # load the plugins that may be found 29 | self.versionedPluginManager.collectPlugins() 30 | # Will be used later 31 | self.plugin_info = None 32 | 33 | def plugin_loading_check(self): 34 | """ 35 | Test if the correct plugin has been loaded. 36 | """ 37 | if self.plugin_info is None: 38 | # check nb of categories 39 | self.assertEqual(len(self.versionedPluginManager.getCategories()),1) 40 | sole_category = self.versionedPluginManager.getCategories()[0] 41 | # check the number of plugins (the older versions of the 42 | # plugins should not be there) 43 | self.assertEqual(len(self.versionedPluginManager.getPluginsOfCategory(sole_category)),1) 44 | # older versions of the plugin should be found in the attic 45 | self.assertEqual(len(self.versionedPluginManager.getPluginsOfCategoryFromAttic(sole_category)),4) 46 | plugins = self.versionedPluginManager.getPluginsOfCategory(sole_category) 47 | self.plugin_info = None 48 | for plugin_info in plugins: 49 | TEST_MESSAGE("plugin info: %s" % plugin_info) 50 | if plugin_info.name == "Versioned Plugin": 51 | self.plugin_info = plugin_info 52 | break 53 | self.assertTrue(self.plugin_info) 54 | # test that the name of the plugin has been correctly defined 55 | self.assertEqual(self.plugin_info.name,"Versioned Plugin") 56 | self.assertEqual(sole_category,self.plugin_info.category) 57 | else: 58 | self.assertTrue(True) 59 | 60 | def testLoaded(self): 61 | """ 62 | Test if the correct plugin has been loaded. 63 | """ 64 | self.plugin_loading_check() 65 | sole_category = self.versionedPluginManager.getCategories()[0] 66 | self.assertEqual(len(self.versionedPluginManager.getLatestPluginsOfCategory(sole_category)),1) 67 | self.plugin_info = self.versionedPluginManager.getLatestPluginsOfCategory(sole_category)[0] 68 | TEST_MESSAGE("plugin info: %s" % self.plugin_info) 69 | # test that the name of the plugin has been correctly defined 70 | self.assertEqual(self.plugin_info.name,"Versioned Plugin") 71 | self.assertEqual(sole_category,self.plugin_info.category) 72 | self.assertEqual("1.2",str(self.plugin_info.version)) 73 | 74 | 75 | def testLatestPluginOfCategory(self): 76 | self.plugin_loading_check() 77 | 78 | def testActivationAndDeactivation(self): 79 | """ 80 | Test if the activation procedure works. 81 | """ 82 | self.plugin_loading_check() 83 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 84 | self.versionedPluginManager.activatePluginByName(self.plugin_info.name, 85 | self.plugin_info.category) 86 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 87 | self.versionedPluginManager.deactivatePluginByName(self.plugin_info.name, 88 | self.plugin_info.category) 89 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 90 | # also check that this is the plugin of the latest version 91 | # that has been activated (ok the following test is already 92 | # ensured by the plugin_loading_check method, but this is to 93 | # make the things clear: the plugin chosen for activation is 94 | # the one with the latest version) 95 | self.assertEqual("1.2",str(self.plugin_info.version)) 96 | 97 | 98 | def testDirectActivationAndDeactivation(self): 99 | """ 100 | Test if the activation procedure works when directly activating a plugin. 101 | """ 102 | self.plugin_loading_check() 103 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 104 | TEST_MESSAGE("plugin object = %s" % self.plugin_info.plugin_object) 105 | self.plugin_info.plugin_object.activate() 106 | self.assertTrue(self.plugin_info.plugin_object.is_activated) 107 | self.plugin_info.plugin_object.deactivate() 108 | self.assertTrue(not self.plugin_info.plugin_object.is_activated) 109 | 110 | 111 | def testAtticConsistencyAfterCategoryFilterUpdate(self): 112 | """ 113 | Test that changing the category filer doesn't make the attic inconsistent. 114 | """ 115 | self.plugin_loading_check() 116 | newCategory = "Mouf" 117 | # Pre-requisite for the test 118 | previousCategories = self.versionedPluginManager.getCategories() 119 | self.assertTrue(len(previousCategories) >= 1) 120 | self.assertTrue(newCategory not in previousCategories) 121 | # change the category and see what's happening 122 | self.versionedPluginManager.setCategoriesFilter({newCategory: IPlugin}) 123 | self.versionedPluginManager.collectPlugins() 124 | for categoryName in previousCategories: 125 | self.assertRaises(KeyError, self.versionedPluginManager\ 126 | .getPluginsOfCategory, categoryName) 127 | self.assertEqual(len(self.versionedPluginManager\ 128 | .getPluginsOfCategoryFromAttic(newCategory)),4) 129 | 130 | 131 | suite = unittest.TestSuite([ 132 | unittest.TestLoader().loadTestsFromTestCase(VersionedTestsCase), 133 | ]) 134 | -------------------------------------------------------------------------------- /package/test/test_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | import os 4 | import sys 5 | 6 | import logging 7 | TEST_MESSAGE = logging.debug 8 | 9 | TEMP_CONFIG_FILE_NAME=os.path.join( 10 | os.path.dirname( 11 | os.path.abspath(__file__)), 12 | "tempconfig") 13 | 14 | # set correct loading path for yapsy's files 15 | sys.path.insert(0, 16 | os.path.dirname( 17 | os.path.dirname( 18 | os.path.abspath(__file__)))) 19 | 20 | sys.path.insert(0, 21 | os.path.dirname( 22 | os.path.dirname( 23 | os.path.dirname( 24 | os.path.abspath(__file__))))) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package/yapsy/AutoInstallPluginManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | Role 5 | ==== 6 | 7 | Defines plugin managers that can handle the installation of plugin 8 | files into the right place. Then the end-user does not have to browse 9 | to the plugin directory to install them. 10 | 11 | API 12 | === 13 | """ 14 | 15 | import os 16 | import shutil 17 | import zipfile 18 | 19 | from yapsy.IPlugin import IPlugin 20 | from yapsy.PluginManagerDecorator import PluginManagerDecorator 21 | from yapsy import log 22 | from io import StringIO 23 | 24 | 25 | class AutoInstallPluginManager(PluginManagerDecorator): 26 | """ 27 | A plugin manager that also manages the installation of the plugin 28 | files into the appropriate directory. 29 | 30 | Ctor Arguments: 31 | 32 | ``plugin_install_dir`` 33 | The directory where new plugins to be installed will be copied. 34 | 35 | .. warning:: If ``plugin_install_dir`` does not correspond to 36 | an element of the ``directories_list``, it is 37 | appended to the later. 38 | """ 39 | 40 | 41 | def __init__(self, 42 | plugin_install_dir=None, 43 | decorated_manager=None, 44 | # The following args will only be used if we need to 45 | # create a default PluginManager 46 | categories_filter=None, 47 | directories_list=None, 48 | plugin_info_ext="yapsy-plugin"): 49 | if categories_filter is None: 50 | categories_filter = {"Default":IPlugin} 51 | # Create the base decorator class 52 | PluginManagerDecorator.__init__(self, 53 | decorated_manager, 54 | categories_filter, 55 | directories_list, 56 | plugin_info_ext) 57 | # set the directory for new plugins 58 | self.plugins_places=[] 59 | self.setInstallDir(plugin_install_dir) 60 | 61 | def setInstallDir(self,plugin_install_dir): 62 | """ 63 | Set the directory where to install new plugins. 64 | """ 65 | if not (plugin_install_dir in self.plugins_places): 66 | self.plugins_places.append(plugin_install_dir) 67 | self.install_dir = plugin_install_dir 68 | 69 | def getInstallDir(self): 70 | """ 71 | Return the directory where new plugins should be installed. 72 | """ 73 | return self.install_dir 74 | 75 | def install(self, directory, plugin_info_filename): 76 | """ 77 | Giving the plugin's info file (e.g. ``myplugin.yapsy-plugin``), 78 | and the directory where it is located, get all the files that 79 | define the plugin and copy them into the correct directory. 80 | 81 | Return ``True`` if the installation is a success, ``False`` if 82 | it is a failure. 83 | """ 84 | # start collecting essential info about the new plugin 85 | plugin_info, config_parser = self._gatherCorePluginInfo(directory, plugin_info_filename) 86 | # now determine the path of the file to execute, 87 | # depending on wether the path indicated is a 88 | # directory or a file 89 | if not (os.path.exists(plugin_info.path) or os.path.exists(plugin_info.path+".py") ): 90 | log.warning("Could not find the plugin's implementation for %s." % plugin_info.name) 91 | return False 92 | if os.path.isdir(plugin_info.path): 93 | try: 94 | shutil.copytree(plugin_info.path, 95 | os.path.join(self.install_dir,os.path.basename(plugin_info.path))) 96 | shutil.copy(os.path.join(directory, plugin_info_filename), 97 | self.install_dir) 98 | except: 99 | log.error("Could not install plugin: %s." % plugin_info.name) 100 | return False 101 | else: 102 | return True 103 | elif os.path.isfile(plugin_info.path+".py"): 104 | try: 105 | shutil.copy(plugin_info.path+".py", 106 | self.install_dir) 107 | shutil.copy(os.path.join(directory, plugin_info_filename), 108 | self.install_dir) 109 | except: 110 | log.error("Could not install plugin: %s." % plugin_info.name) 111 | return False 112 | else: 113 | return True 114 | else: 115 | return False 116 | 117 | 118 | def installFromZIP(self, plugin_ZIP_filename): 119 | """ 120 | Giving the plugin's zip file (e.g. ``myplugin.zip``), check 121 | that their is a valid info file in it and correct all the 122 | plugin files into the correct directory. 123 | 124 | .. warning:: Only available for python 2.6 and later. 125 | 126 | Return ``True`` if the installation is a success, ``False`` if 127 | it is a failure. 128 | """ 129 | if not os.path.isfile(plugin_ZIP_filename): 130 | log.warning("Could not find the plugin's zip file at '%s'." % plugin_ZIP_filename) 131 | return False 132 | try: 133 | candidateZipFile = zipfile.ZipFile(plugin_ZIP_filename) 134 | first_bad_file = candidateZipFile.testzip() 135 | if first_bad_file: 136 | raise Exception("Corrupted ZIP with first bad file '%s'" % first_bad_file) 137 | except Exception as e: 138 | log.warning("Invalid zip file '%s' (error: %s)." % (plugin_ZIP_filename,e)) 139 | return False 140 | zipContent = candidateZipFile.namelist() 141 | log.info("Investigating the content of a zip file containing: '%s'" % zipContent) 142 | log.info("Sanity checks on zip's contained files (looking for hazardous path symbols).") 143 | # check absence of root path and ".." shortcut that would 144 | # send the file oustide the desired directory 145 | for containedFileName in zipContent: 146 | # WARNING: the sanity checks below are certainly not 147 | # exhaustive (maybe we could do something a bit smarter by 148 | # using os.path.expanduser, os.path.expandvars and 149 | # os.path.normpath) 150 | if containedFileName.startswith("/"): 151 | log.warning("Unsecure zip file, rejected because one of its file paths ('%s') starts with '/'" % containedFileName) 152 | return False 153 | if containedFileName.startswith(r"\\") or containedFileName.startswith("//"): 154 | log.warning(r"Unsecure zip file, rejected because one of its file paths ('%s') starts with '\\'" % containedFileName) 155 | return False 156 | if os.path.splitdrive(containedFileName)[0]: 157 | log.warning("Unsecure zip file, rejected because one of its file paths ('%s') starts with a drive letter" % containedFileName) 158 | return False 159 | if os.path.isabs(containedFileName): 160 | log.warning("Unsecure zip file, rejected because one of its file paths ('%s') is absolute" % containedFileName) 161 | return False 162 | pathComponent = os.path.split(containedFileName) 163 | if ".." in pathComponent: 164 | log.warning("Unsecure zip file, rejected because one of its file paths ('%s') contains '..'" % containedFileName) 165 | return False 166 | if "~" in pathComponent: 167 | log.warning("Unsecure zip file, rejected because one of its file paths ('%s') contains '~'" % containedFileName) 168 | return False 169 | infoFileCandidates = [filename for filename in zipContent if os.path.dirname(filename)==""] 170 | if not infoFileCandidates: 171 | log.warning("Zip file structure seems wrong in '%s', no info file found." % plugin_ZIP_filename) 172 | return False 173 | isValid = False 174 | log.info("Looking for the zipped plugin's info file among '%s'" % infoFileCandidates) 175 | for infoFileName in infoFileCandidates: 176 | infoFile = candidateZipFile.read(infoFileName) 177 | log.info("Assuming the zipped plugin info file to be '%s'" % infoFileName) 178 | pluginName,moduleName,_ = self._getPluginNameAndModuleFromStream(StringIO(str(infoFile,encoding="utf-8"))) 179 | if moduleName is None: 180 | continue 181 | log.info("Checking existence of the expected module '%s' in the zip file" % moduleName) 182 | candidate_module_paths = [ 183 | moduleName, 184 | # Try path consistent with the platform specific one 185 | os.path.join(moduleName,"__init__.py"), 186 | # Try typical paths (unix and windows) 187 | "%s/__init__.py" % moduleName, 188 | "%s\\__init__.py" % moduleName 189 | ] 190 | for candidate in candidate_module_paths: 191 | if candidate in zipContent: 192 | isValid = True 193 | break 194 | if isValid: 195 | break 196 | if not isValid: 197 | log.warning("Zip file structure seems wrong in '%s', " 198 | "could not match info file with the implementation of plugin '%s'." % (plugin_ZIP_filename,pluginName)) 199 | return False 200 | else: 201 | try: 202 | candidateZipFile.extractall(self.install_dir) 203 | return True 204 | except Exception as e: 205 | log.error("Could not install plugin '%s' from zip file '%s' (exception: '%s')." % (pluginName,plugin_ZIP_filename,e)) 206 | return False 207 | 208 | -------------------------------------------------------------------------------- /package/yapsy/ConfigurablePluginManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | Role 5 | ==== 6 | 7 | Defines plugin managers that can handle configuration files similar to 8 | the ini files manipulated by Python's ConfigParser module. 9 | 10 | API 11 | === 12 | """ 13 | 14 | from yapsy.IPlugin import IPlugin 15 | 16 | 17 | from yapsy.PluginManagerDecorator import PluginManagerDecorator 18 | from yapsy.PluginManager import PLUGIN_NAME_FORBIDEN_STRING 19 | 20 | 21 | class ConfigurablePluginManager(PluginManagerDecorator): 22 | """ 23 | A plugin manager that also manages a configuration file. 24 | 25 | The configuration file will be accessed through a ``ConfigParser`` 26 | derivated object. The file can be used for other purpose by the 27 | application using this plugin manager as it will only add a new 28 | specific section ``[Plugin Management]`` for itself and also new 29 | sections for some plugins that will start with ``[Plugin:...]`` 30 | (only the plugins that explicitly requires to save configuration 31 | options will have this kind of section). 32 | 33 | .. warning:: when giving/building the list of plugins to activate 34 | by default, there must not be any space in the list 35 | (neither in the names nor in between) 36 | 37 | The ``config_change_trigger`` argument can be used to set a 38 | specific method to call when the configuration is 39 | altered. This will let the client application manage the way 40 | they want the configuration to be updated (e.g. write on file 41 | at each change or at precise time intervalls or whatever....) 42 | 43 | .. warning:: when no ``config_change_trigger`` is given and if 44 | the provided ``configparser_instance`` doesn't handle it 45 | implicitely, recording the changes persistently (ie writing on 46 | the config file) won't happen. 47 | """ 48 | 49 | CONFIG_SECTION_NAME = "Plugin Management" 50 | 51 | 52 | def __init__(self, 53 | configparser_instance=None, 54 | config_change_trigger= lambda :True, 55 | decorated_manager=None, 56 | # The following args will only be used if we need to 57 | # create a default PluginManager 58 | categories_filter=None, 59 | directories_list=None, 60 | plugin_info_ext="yapsy-plugin"): 61 | if categories_filter is None: 62 | categories_filter = {"Default":IPlugin} 63 | # Create the base decorator class 64 | PluginManagerDecorator.__init__(self,decorated_manager, 65 | categories_filter, 66 | directories_list, 67 | plugin_info_ext) 68 | self.setConfigParser(configparser_instance, config_change_trigger) 69 | 70 | 71 | def setConfigParser(self,configparser_instance,config_change_trigger): 72 | """ 73 | Set the ConfigParser instance. 74 | """ 75 | self.config_parser = configparser_instance 76 | # set the (optional) fucntion to be called when the 77 | # configuration is changed: 78 | self.config_has_changed = config_change_trigger 79 | 80 | def __getCategoryPluginsListFromConfig(self, plugin_list_str): 81 | """ 82 | Parse the string describing the list of plugins to activate, 83 | to discover their actual names and return them. 84 | """ 85 | return plugin_list_str.strip(" ").split("%s"%PLUGIN_NAME_FORBIDEN_STRING) 86 | 87 | def __getCategoryPluginsConfigFromList(self, plugin_list): 88 | """ 89 | Compose a string describing the list of plugins to activate 90 | """ 91 | return PLUGIN_NAME_FORBIDEN_STRING.join(plugin_list) 92 | 93 | def __getCategoryOptionsName(self,category_name): 94 | """ 95 | Return the appropirately formated version of the category's 96 | option. 97 | """ 98 | return "%s_plugins_to_load" % category_name.replace(" ","_") 99 | 100 | def __addPluginToConfig(self,category_name, plugin_name): 101 | """ 102 | Utility function to add a plugin to the list of plugin to be 103 | activated. 104 | """ 105 | # check that the section is here 106 | if not self.config_parser.has_section(self.CONFIG_SECTION_NAME): 107 | self.config_parser.add_section(self.CONFIG_SECTION_NAME) 108 | # check that the category's list of activated plugins is here too 109 | option_name = self.__getCategoryOptionsName(category_name) 110 | if not self.config_parser.has_option(self.CONFIG_SECTION_NAME, option_name): 111 | # if there is no list yet add a new one 112 | self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,plugin_name) 113 | return self.config_has_changed() 114 | else: 115 | # get the already existing list and append the new 116 | # activated plugin to it. 117 | past_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME,option_name) 118 | past_list = self.__getCategoryPluginsListFromConfig(past_list_str) 119 | # make sure we don't add it twice 120 | if plugin_name not in past_list: 121 | past_list.append(plugin_name) 122 | new_list_str = self.__getCategoryPluginsConfigFromList(past_list) 123 | self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,new_list_str) 124 | return self.config_has_changed() 125 | 126 | def __removePluginFromConfig(self,category_name, plugin_name): 127 | """ 128 | Utility function to add a plugin to the list of plugin to be 129 | activated. 130 | """ 131 | # check that the section is here 132 | if not self.config_parser.has_section(self.CONFIG_SECTION_NAME): 133 | # then nothing to remove :) 134 | return 135 | # check that the category's list of activated plugins is here too 136 | option_name = self.__getCategoryOptionsName(category_name) 137 | if not self.config_parser.has_option(self.CONFIG_SECTION_NAME, option_name): 138 | # if there is no list still nothing to do 139 | return 140 | else: 141 | # get the already existing list 142 | past_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME,option_name) 143 | past_list = self.__getCategoryPluginsListFromConfig(past_list_str) 144 | if plugin_name in past_list: 145 | past_list.remove(plugin_name) 146 | new_list_str = self.__getCategoryPluginsConfigFromList(past_list) 147 | self.config_parser.set(self.CONFIG_SECTION_NAME,option_name,new_list_str) 148 | self.config_has_changed() 149 | 150 | 151 | 152 | def registerOptionFromPlugin(self, 153 | category_name, plugin_name, 154 | option_name, option_value): 155 | """ 156 | To be called from a plugin object, register a given option in 157 | the name of a given plugin. 158 | """ 159 | section_name = "%s Plugin: %s" % (category_name,plugin_name) 160 | # if the plugin's section is not here yet, create it 161 | if not self.config_parser.has_section(section_name): 162 | self.config_parser.add_section(section_name) 163 | # set the required option 164 | self.config_parser.set(section_name,option_name,option_value) 165 | self.config_has_changed() 166 | 167 | def hasOptionFromPlugin(self, 168 | category_name, plugin_name, option_name): 169 | """ 170 | To be called from a plugin object, return True if the option 171 | has already been registered. 172 | """ 173 | section_name = "%s Plugin: %s" % (category_name,plugin_name) 174 | return self.config_parser.has_section(section_name) and self.config_parser.has_option(section_name,option_name) 175 | 176 | def readOptionFromPlugin(self, 177 | category_name, plugin_name, option_name): 178 | """ 179 | To be called from a plugin object, read a given option in 180 | the name of a given plugin. 181 | """ 182 | section_name = "%s Plugin: %s" % (category_name,plugin_name) 183 | return self.config_parser.get(section_name,option_name) 184 | 185 | 186 | def __decoratePluginObject(self, category_name, plugin_name, plugin_object): 187 | """ 188 | Add two methods to the plugin objects that will make it 189 | possible for it to benefit from this class's api concerning 190 | the management of the options. 191 | """ 192 | plugin_object.setConfigOption = lambda x,y: self.registerOptionFromPlugin(category_name, 193 | plugin_name, 194 | x,y) 195 | plugin_object.setConfigOption.__doc__ = self.registerOptionFromPlugin.__doc__ 196 | plugin_object.getConfigOption = lambda x: self.readOptionFromPlugin(category_name, 197 | plugin_name, 198 | x) 199 | plugin_object.getConfigOption.__doc__ = self.readOptionFromPlugin.__doc__ 200 | plugin_object.hasConfigOption = lambda x: self.hasOptionFromPlugin(category_name, 201 | plugin_name, 202 | x) 203 | plugin_object.hasConfigOption.__doc__ = self.hasOptionFromPlugin.__doc__ 204 | 205 | def activatePluginByName(self, plugin_name, category_name="Default", save_state=True): 206 | """ 207 | Activate a plugin, , and remember it (in the config file). 208 | 209 | If you want the plugin to benefit from the configuration 210 | utility defined by this manager, it is crucial to use this 211 | method to activate a plugin and not call the plugin object's 212 | ``activate`` method. In fact, this method will also "decorate" 213 | the plugin object so that it can use this class's methods to 214 | register its own options. 215 | 216 | By default, the plugin's activation is registered in the 217 | config file but if you d'ont want this set the 'save_state' 218 | argument to False. 219 | """ 220 | # first decorate the plugin 221 | pta = self._component.getPluginByName(plugin_name,category_name) 222 | if pta is None: 223 | return None 224 | self.__decoratePluginObject(category_name,plugin_name,pta.plugin_object) 225 | # activate the plugin 226 | plugin_object = self._component.activatePluginByName(plugin_name,category_name) 227 | # check the activation and then optionally set the config option 228 | if plugin_object.is_activated: 229 | if save_state: 230 | self.__addPluginToConfig(category_name,plugin_name) 231 | return plugin_object 232 | return None 233 | 234 | def deactivatePluginByName(self, plugin_name, category_name="Default", save_state=True): 235 | """ 236 | Deactivate a plugin, and remember it (in the config file). 237 | 238 | By default, the plugin's deactivation is registered in the 239 | config file but if you d'ont want this set the ``save_state`` 240 | argument to False. 241 | """ 242 | # activate the plugin 243 | plugin_object = self._component.deactivatePluginByName(plugin_name,category_name) 244 | if plugin_object is None: 245 | return None 246 | # check the deactivation and then optionnally set the config option 247 | if not plugin_object.is_activated: 248 | if save_state: 249 | self.__removePluginFromConfig(category_name,plugin_name) 250 | return plugin_object 251 | return None 252 | 253 | def loadPlugins(self,callback=None, callback_after=None): 254 | """ 255 | Walk through the plugins' places and look for plugins. Then 256 | for each plugin candidate look for its category, load it and 257 | stores it in the appropriate slot of the ``category_mapping``. 258 | """ 259 | self._component.loadPlugins(callback, callback_after) 260 | # now load the plugins according to the recorded configuration 261 | if self.config_parser.has_section(self.CONFIG_SECTION_NAME): 262 | # browse all the categories 263 | for category_name in list(self._component.category_mapping.keys()): 264 | # get the list of plugins to be activated for this 265 | # category 266 | option_name = "%s_plugins_to_load"%category_name 267 | if self.config_parser.has_option(self.CONFIG_SECTION_NAME, 268 | option_name): 269 | plugin_list_str = self.config_parser.get(self.CONFIG_SECTION_NAME, 270 | option_name) 271 | plugin_list = self.__getCategoryPluginsListFromConfig(plugin_list_str) 272 | # activate all the plugins that should be 273 | # activated 274 | for plugin_name in plugin_list: 275 | self.activatePluginByName(plugin_name,category_name) 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /package/yapsy/FilteredPluginManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | Role 5 | ==== 6 | 7 | Defines the basic mechanisms to have a plugin manager filter the 8 | available list of plugins after locating them and before loading them. 9 | 10 | One use fo this would be to prevent untrusted plugins from entering 11 | the system. 12 | 13 | To use it properly you must reimplement or monkey patch the 14 | ``IsPluginOk`` method, as in the following example:: 15 | 16 | # define a plugin manager (with you prefered options) 17 | pm = PluginManager(...) 18 | # decorate it with the Filtering mechanics 19 | pm = FilteredPluginManager(pm) 20 | # define a custom predicate that filters out plugins without descriptions 21 | pm.isPluginOk = lambda x: x.description!="" 22 | 23 | 24 | API 25 | === 26 | """ 27 | 28 | 29 | from yapsy.IPlugin import IPlugin 30 | from yapsy.PluginManagerDecorator import PluginManagerDecorator 31 | 32 | 33 | class FilteredPluginManager(PluginManagerDecorator): 34 | """ 35 | Base class for decorators which filter the plugins list 36 | before they are loaded. 37 | """ 38 | 39 | def __init__(self, 40 | decorated_manager=None, 41 | categories_filter=None, 42 | directories_list=None, 43 | plugin_info_ext="yapsy-plugin"): 44 | if categories_filter is None: 45 | categories_filter = {"Default":IPlugin} 46 | # Create the base decorator class 47 | PluginManagerDecorator.__init__(self,decorated_manager, 48 | categories_filter, 49 | directories_list, 50 | plugin_info_ext) 51 | # prepare the mapping of the latest version of each plugin 52 | self.rejectedPlugins = [ ] 53 | 54 | 55 | 56 | def filterPlugins(self): 57 | """ 58 | Go through the currently available candidates, and and either 59 | leaves them, or moves them into the list of rejected Plugins. 60 | 61 | Can be overridden if overriding ``isPluginOk`` sentinel is not 62 | powerful enough. 63 | """ 64 | self.rejectedPlugins = [ ] 65 | for candidate_infofile, candidate_filepath, plugin_info in self._component.getPluginCandidates(): 66 | if not self.isPluginOk( plugin_info): 67 | self.rejectPluginCandidate((candidate_infofile, candidate_filepath, plugin_info) ) 68 | 69 | def rejectPluginCandidate(self,pluginTuple): 70 | """ 71 | Move a plugin from the candidates list to the rejected List. 72 | """ 73 | if pluginTuple in self.getPluginCandidates(): 74 | self._component.removePluginCandidate(pluginTuple) 75 | if not pluginTuple in self.rejectedPlugins: 76 | self.rejectedPlugins.append(pluginTuple) 77 | 78 | def unrejectPluginCandidate(self,pluginTuple): 79 | """ 80 | Move a plugin from the rejected list to into the candidates 81 | list. 82 | """ 83 | if not pluginTuple in self.getPluginCandidates(): 84 | self._component.appendPluginCandidate(pluginTuple) 85 | if pluginTuple in self.rejectedPlugins: 86 | self.rejectedPlugins.remove(pluginTuple) 87 | 88 | def removePluginCandidate(self,pluginTuple): 89 | """ 90 | Remove a plugin from the list of candidates. 91 | """ 92 | if pluginTuple in self.getPluginCandidates(): 93 | self._component.removePluginCandidate(pluginTuple) 94 | if pluginTuple in self.rejectedPlugins: 95 | self.rejectedPlugins.remove(pluginTuple) 96 | 97 | 98 | def appendPluginCandidate(self,pluginTuple): 99 | """ 100 | Add a new candidate. 101 | """ 102 | if self.isPluginOk(pluginTuple[2]): 103 | if pluginTuple not in self.getPluginCandidates(): 104 | self._component.appendPluginCandidate(pluginTuple) 105 | else: 106 | if not pluginTuple in self.rejectedPlugins: 107 | self.rejectedPlugins.append(pluginTuple) 108 | 109 | def isPluginOk(self,info): 110 | """ 111 | Sentinel function to detect if a plugin should be filtered. 112 | 113 | ``info`` is an instance of a ``PluginInfo`` and this method is 114 | expected to return True if the corresponding plugin can be 115 | accepted, and False if it must be filtered out. 116 | 117 | Subclasses should override this function and return false for 118 | any plugin which they do not want to be loadable. 119 | """ 120 | return True 121 | 122 | def locatePlugins(self): 123 | """ 124 | locate and filter plugins. 125 | """ 126 | #Reset Catalogue 127 | self.setCategoriesFilter(self._component.categories_interfaces) 128 | #Reread and filter. 129 | self._component.locatePlugins() 130 | self.filterPlugins() 131 | return len(self._component.getPluginCandidates()) 132 | 133 | def getRejectedPlugins(self): 134 | """ 135 | Return the list of rejected plugins. 136 | """ 137 | return self.rejectedPlugins[:] 138 | -------------------------------------------------------------------------------- /package/yapsy/IMultiprocessChildPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | Originally defined the basic interfaces for multiprocessed plugins. 9 | 10 | Deprecation Note 11 | ================ 12 | 13 | This class is deprecated and replaced by :doc:`IMultiprocessChildPlugin`. 14 | 15 | Child classes of `IMultiprocessChildPlugin` used to be an `IPlugin` as well as 16 | a `multiprocessing.Process`, possibly playing with the functionalities of both, 17 | which make maintenance harder than necessary. 18 | 19 | And indeed following a bug fix to make multiprocess plugins work on Windows, 20 | instances of IMultiprocessChildPlugin inherit Process but are not exactly the 21 | running process (there is a new wrapper process). 22 | 23 | API 24 | === 25 | """ 26 | 27 | from multiprocessing import Process 28 | from yapsy.IMultiprocessPlugin import IMultiprocessPlugin 29 | 30 | 31 | class IMultiprocessChildPlugin(IMultiprocessPlugin, Process): 32 | """ 33 | Base class for multiprocessed plugin. 34 | 35 | DEPRECATED(>1.11): Please use IMultiProcessPluginBase instead ! 36 | """ 37 | 38 | def __init__(self, parent_pipe): 39 | IMultiprocessPlugin.__init__(self, parent_pipe) 40 | Process.__init__(self) 41 | 42 | def run(self): 43 | """ 44 | Override this method in your implementation 45 | """ 46 | return 47 | -------------------------------------------------------------------------------- /package/yapsy/IMultiprocessPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | Defines the basic interfaces for multiprocessed plugins. 9 | 10 | Extensibility 11 | ============= 12 | 13 | In your own software, you'll probably want to build derived classes of 14 | the ``IMultiprocessPlugin`` class as it is a mere interface with no specific 15 | functionality. 16 | 17 | Your software's plugins should then inherit your very own plugin class 18 | (itself derived from ``IMultiprocessPlugin``). 19 | 20 | Override the `run` method to include your code. Use the `self.parent_pipe` to send 21 | and receive data with the parent process or create your own communication 22 | mecanism. 23 | 24 | Where and how to code these plugins is explained in the section about 25 | the :doc:`PluginManager`. 26 | 27 | API 28 | === 29 | """ 30 | 31 | from yapsy.IPlugin import IPlugin 32 | 33 | 34 | class IMultiprocessPlugin(IPlugin): 35 | """ 36 | Base class for multiprocessed plugin. 37 | """ 38 | 39 | def __init__(self, parent_pipe): 40 | IPlugin.__init__(self) 41 | self.parent_pipe = parent_pipe 42 | 43 | def run(self): 44 | """ 45 | Override this method in your implementation 46 | """ 47 | return 48 | -------------------------------------------------------------------------------- /package/yapsy/IPlugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | Defines the basic interfaces for a plugin. These interfaces are 9 | inherited by the *core* class of a plugin. The *core* class of a 10 | plugin is then the one that will be notified the 11 | activation/deactivation of a plugin via the ``activate/deactivate`` 12 | methods. 13 | 14 | 15 | For simple (near trivial) plugin systems, one can directly use the 16 | following interfaces. 17 | 18 | Extensibility 19 | ============= 20 | 21 | In your own software, you'll probably want to build derived classes of 22 | the ``IPlugin`` class as it is a mere interface with no specific 23 | functionality. 24 | 25 | Your software's plugins should then inherit your very own plugin class 26 | (itself derived from ``IPlugin``). 27 | 28 | Where and how to code these plugins is explained in the section about 29 | the :doc:`PluginManager`. 30 | 31 | 32 | API 33 | === 34 | """ 35 | 36 | 37 | class IPlugin(object): 38 | """ 39 | The most simple interface to be inherited when creating a plugin. 40 | """ 41 | 42 | def __init__(self): 43 | self.is_activated = False 44 | 45 | def activate(self): 46 | """ 47 | Called at plugin activation. 48 | """ 49 | self.is_activated = True 50 | 51 | def deactivate(self): 52 | """ 53 | Called when the plugin is disabled. 54 | """ 55 | self.is_activated = False 56 | 57 | -------------------------------------------------------------------------------- /package/yapsy/IPluginLocator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | 5 | """ 6 | Role 7 | ==== 8 | 9 | ``IPluginLocator`` defines the basic interface expected by a 10 | ``PluginManager`` to be able to locate plugins and get basic info 11 | about each discovered plugin (name, version etc). 12 | 13 | API 14 | === 15 | 16 | """ 17 | 18 | 19 | from yapsy import log 20 | 21 | class IPluginLocator(object): 22 | """ 23 | Plugin Locator interface with some methods already implemented to 24 | manage the awkward backward compatible stuff. 25 | """ 26 | 27 | def locatePlugins(self): 28 | """ 29 | Walk through the plugins' places and look for plugins. 30 | 31 | Return the discovered plugins as a list of 32 | ``(candidate_infofile_path, candidate_file_path,plugin_info_instance)`` 33 | and their number. 34 | """ 35 | raise NotImplementedError("locatePlugins must be reimplemented by %s" % self) 36 | 37 | def gatherCorePluginInfo(self, directory, filename): 38 | """ 39 | Return a ``PluginInfo`` as well as the ``ConfigParser`` used to build it. 40 | 41 | If filename is a valid plugin discovered by any of the known 42 | strategy in use. Returns None,None otherwise. 43 | """ 44 | raise NotImplementedError("gatherPluginInfo must be reimplemented by %s" % self) 45 | 46 | # -------------------------------------------------------------------- 47 | # Below are backward compatibility methods: if you inherit from 48 | # IPluginLocator it's ok not to reimplement them, there will only 49 | # be a warning message logged if they are called and not 50 | # reimplemented. 51 | # -------------------------------------------------------------------- 52 | 53 | def getPluginNameAndModuleFromStream(self,fileobj): 54 | """ 55 | DEPRECATED(>1.9): kept for backward compatibility 56 | with existing PluginManager child classes. 57 | 58 | Return a 3-uple with the name of the plugin, its 59 | module and the config_parser used to gather the core 60 | data *in a tuple*, if the required info could be 61 | localised, else return ``(None,None,None)``. 62 | """ 63 | log.warning("setPluginInfoClass was called but '%s' doesn't implement it." % self) 64 | return None,None,None 65 | 66 | 67 | def setPluginInfoClass(self, picls, names=None): 68 | """ 69 | DEPRECATED(>1.9): kept for backward compatibility 70 | with existing PluginManager child classes. 71 | 72 | Set the class that holds PluginInfo. The class should inherit 73 | from ``PluginInfo``. 74 | """ 75 | log.warning("setPluginInfoClass was called but '%s' doesn't implement it." % self) 76 | 77 | def getPluginInfoClass(self): 78 | """ 79 | DEPRECATED(>1.9): kept for backward compatibility 80 | with existing PluginManager child classes. 81 | 82 | Get the class that holds PluginInfo. 83 | """ 84 | log.warning("getPluginInfoClass was called but '%s' doesn't implement it." % self) 85 | return None 86 | 87 | def setPluginPlaces(self, directories_list): 88 | """ 89 | DEPRECATED(>1.9): kept for backward compatibility 90 | with existing PluginManager child classes. 91 | 92 | Set the list of directories where to look for plugin places. 93 | """ 94 | log.warning("setPluginPlaces was called but '%s' doesn't implement it." % self) 95 | 96 | def updatePluginPlaces(self, directories_list): 97 | """ 98 | DEPRECATED(>1.9): kept for backward compatibility 99 | with existing PluginManager child classes. 100 | 101 | Updates the list of directories where to look for plugin places. 102 | """ 103 | log.warning("updatePluginPlaces was called but '%s' doesn't implement it." % self) 104 | 105 | -------------------------------------------------------------------------------- /package/yapsy/MultiprocessPluginManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | Role 5 | ==== 6 | 7 | Defines a plugin manager that runs all plugins in separate process 8 | linked by pipes. 9 | 10 | 11 | API 12 | === 13 | """ 14 | 15 | import multiprocessing as mproc 16 | 17 | from yapsy.IMultiprocessPlugin import IMultiprocessPlugin 18 | from yapsy.IMultiprocessChildPlugin import IMultiprocessChildPlugin 19 | from yapsy.MultiprocessPluginProxy import MultiprocessPluginProxy 20 | from yapsy.PluginManager import PluginManager 21 | 22 | 23 | class MultiprocessPluginManager(PluginManager): 24 | """ 25 | Subclass of the PluginManager that runs each plugin in a different process 26 | """ 27 | 28 | def __init__(self, 29 | categories_filter=None, 30 | directories_list=None, 31 | plugin_info_ext=None, 32 | plugin_locator=None): 33 | if categories_filter is None: 34 | categories_filter = {"Default": IMultiprocessPlugin} 35 | PluginManager.__init__(self, 36 | categories_filter=categories_filter, 37 | directories_list=directories_list, 38 | plugin_info_ext=plugin_info_ext, 39 | plugin_locator=plugin_locator) 40 | self.connections = [] 41 | 42 | 43 | def instanciateElementWithImportInfo(self, element, element_name, 44 | plugin_module_name, candidate_filepath): 45 | """This method instanciates each plugin in a new process and links it to 46 | the parent with a pipe. 47 | 48 | In the parent process context, the plugin's class is replaced by 49 | the ``MultiprocessPluginProxy`` class that hold the information 50 | about the child process and the pipe to communicate with it. 51 | 52 | .. warning:: 53 | The plugin code should only use the pipe to 54 | communicate with the rest of the applica`tion and should not 55 | assume any kind of shared memory, not any specific functionality 56 | of the `multiprocessing.Process` parent class (its behaviour is 57 | different between platforms !) 58 | 59 | See ``IMultiprocessPlugin``. 60 | """ 61 | if element is IMultiprocessChildPlugin: 62 | # The following will keep retro compatibility for IMultiprocessChildPlugin 63 | raise Exception("Preventing instanciation of a bar child plugin interface.") 64 | instanciated_element = MultiprocessPluginProxy() 65 | parent_pipe, child_pipe = mproc.Pipe() 66 | instanciated_element.child_pipe = parent_pipe 67 | instanciated_element.proc = MultiprocessPluginManager._PluginProcessWrapper( 68 | element_name, plugin_module_name, candidate_filepath, 69 | child_pipe) 70 | instanciated_element.proc.start() 71 | return instanciated_element 72 | 73 | 74 | class _PluginProcessWrapper(mproc.Process): 75 | """Helper class that strictly needed to be able to spawn the 76 | plugin on Windows but kept also for Unix platform to get a more 77 | uniform behaviour. 78 | 79 | This will handle re-importing the plugin's module in the child 80 | process (again this is necessary on windows because what has 81 | been imported in the main thread/process will not be shared with 82 | the spawned process.) 83 | """ 84 | def __init__(self, element_name, plugin_module_name, candidate_filepath, child_pipe): 85 | self.element_name = element_name 86 | self.child_pipe = child_pipe 87 | self.plugin_module_name = plugin_module_name 88 | self.candidate_filepath = candidate_filepath 89 | mproc.Process.__init__(self) 90 | 91 | def run(self): 92 | module = PluginManager._importModule(self.plugin_module_name, 93 | self.candidate_filepath) 94 | element = getattr(module, self.element_name) 95 | e = element(self.child_pipe) 96 | e.run() 97 | -------------------------------------------------------------------------------- /package/yapsy/MultiprocessPluginProxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | The ``MultiprocessPluginProxy`` is instanciated by the MultiprocessPluginManager to replace the real implementation 9 | that is run in a different process. 10 | 11 | You cannot access your plugin directly from the parent process. You should use the child_pipe to communicate 12 | with your plugin. The `MultiprocessPluginProxy`` role is to keep reference of the communication pipe to the 13 | child process as well as the process informations. 14 | 15 | API 16 | === 17 | """ 18 | 19 | from yapsy.IPlugin import IPlugin 20 | 21 | 22 | class MultiprocessPluginProxy(IPlugin): 23 | """ 24 | This class contains two members that are initialized by the :doc:`MultiprocessPluginManager`. 25 | 26 | self.proc is a reference that holds the multiprocessing.Process instance of the child process. 27 | 28 | self.child_pipe is a reference that holds the multiprocessing.Pipe instance to communicate with the child. 29 | """ 30 | def __init__(self): 31 | IPlugin.__init__(self) 32 | self.proc = None # This attribute holds the multiprocessing.Process instance 33 | self.child_pipe = None # This attribute holds the multiprocessing.Pipe instance 34 | -------------------------------------------------------------------------------- /package/yapsy/PluginInfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | Encapsulate a plugin instance as well as some metadata. 9 | 10 | API 11 | === 12 | """ 13 | 14 | 15 | from configparser import ConfigParser 16 | from packaging.version import Version 17 | 18 | 19 | class PluginInfo(object): 20 | """Representation of the most basic set of information related to a 21 | given plugin such as its name, author, description... 22 | 23 | Any additional information can be stored ad retrieved in a 24 | PluginInfo, when this one is created with a 25 | ``ConfigParser.ConfigParser`` instance. 26 | 27 | This typically means that when metadata is read from a text file 28 | (the original way for yapsy to describe plugins), all info that is 29 | not part of the basic variables (name, path, version etc), can 30 | still be accessed though the ``details`` member variables that 31 | behaves like Python's ``ConfigParser.ConfigParser``. 32 | 33 | .. warning:: 34 | The instance associated with the ``details`` member 35 | variable is never copied and used to store all plugin infos. If 36 | you set it to a custom instance, it will be modified as soon as 37 | another member variale of the plugin info is 38 | changed. Alternatively, if you change the instance "outside" the 39 | plugin info, it will also change the plugin info. 40 | 41 | Ctor Arguments: 42 | 43 | :plugin_name: is a simple string describing the name of 44 | the plugin. 45 | 46 | :plugin_path: describe the location where the plugin can be 47 | found. 48 | 49 | .. warning:: 50 | The ``path`` attribute is the full path to the 51 | plugin if it is organised as a directory or the 52 | full path to a file without the ``.py`` extension 53 | if the plugin is defined by a simple file. In the 54 | later case, the actual plugin is reached via 55 | ``plugin_info.path+'.py'``. 56 | """ 57 | 58 | def __init__(self, plugin_name, plugin_path): 59 | self.__details = ConfigParser() 60 | self.name = plugin_name 61 | self.path = plugin_path 62 | self._ensureDetailsDefaultsAreBackwardCompatible() 63 | # Storage for stuff created during the plugin lifetime 64 | self.plugin_object = None 65 | self.categories = [] 66 | self.error = None 67 | 68 | 69 | def __setDetails(self,cfDetails): 70 | """ 71 | Fill in all details by storing a ``ConfigParser`` instance. 72 | 73 | .. warning:: 74 | The values for ``plugin_name`` and 75 | ``plugin_path`` given a init time will superseed 76 | any value found in ``cfDetails`` in section 77 | 'Core' for the options 'Name' and 'Module' (this 78 | is mostly for backward compatibility). 79 | """ 80 | bkp_name = self.name 81 | bkp_path = self.path 82 | self.__details = cfDetails 83 | self.name = bkp_name 84 | self.path = bkp_path 85 | self._ensureDetailsDefaultsAreBackwardCompatible() 86 | 87 | def __getDetails(self): 88 | return self.__details 89 | 90 | def __getName(self): 91 | return self.details.get("Core","Name") 92 | 93 | def __setName(self, name): 94 | if not self.details.has_section("Core"): 95 | self.details.add_section("Core") 96 | self.details.set("Core","Name",name) 97 | 98 | 99 | def __getPath(self): 100 | return self.details.get("Core","Module") 101 | 102 | def __setPath(self,path): 103 | if not self.details.has_section("Core"): 104 | self.details.add_section("Core") 105 | self.details.set("Core","Module",path) 106 | 107 | 108 | def __getVersion(self): 109 | return Version(self.details.get("Documentation","Version")) 110 | 111 | def setVersion(self, vstring): 112 | """ 113 | Set the version of the plugin. 114 | 115 | Used by subclasses to provide different handling of the 116 | version number. 117 | """ 118 | if isinstance(vstring,Version): 119 | vstring = str(vstring) 120 | if not self.details.has_section("Documentation"): 121 | self.details.add_section("Documentation") 122 | self.details.set("Documentation","Version",vstring) 123 | 124 | def __getAuthor(self): 125 | return self.details.get("Documentation","Author") 126 | 127 | def __setAuthor(self,author): 128 | if not self.details.has_section("Documentation"): 129 | self.details.add_section("Documentation") 130 | self.details.set("Documentation","Author",author) 131 | 132 | 133 | def __getCopyright(self): 134 | return self.details.get("Documentation","Copyright") 135 | 136 | def __setCopyright(self,copyrightTxt): 137 | if not self.details.has_section("Documentation"): 138 | self.details.add_section("Documentation") 139 | self.details.set("Documentation","Copyright",copyrightTxt) 140 | 141 | 142 | def __getWebsite(self): 143 | return self.details.get("Documentation","Website") 144 | 145 | def __setWebsite(self,website): 146 | if not self.details.has_section("Documentation"): 147 | self.details.add_section("Documentation") 148 | self.details.set("Documentation","Website",website) 149 | 150 | 151 | def __getDescription(self): 152 | return self.details.get("Documentation","Description") 153 | 154 | def __setDescription(self,description): 155 | if not self.details.has_section("Documentation"): 156 | self.details.add_section("Documentation") 157 | return self.details.set("Documentation","Description",description) 158 | 159 | 160 | def __getCategory(self): 161 | """ 162 | DEPRECATED (>1.9): Mimic former behaviour when what is 163 | noz the first category was considered as the only one the 164 | plugin belonged to. 165 | """ 166 | if self.categories: 167 | return self.categories[0] 168 | else: 169 | return "UnknownCategory" 170 | 171 | def __setCategory(self,c): 172 | """ 173 | DEPRECATED (>1.9): Mimic former behaviour by making so 174 | that if a category is set as if it were the only category to 175 | which the plugin belongs, then a __getCategory will return 176 | this newly set category. 177 | """ 178 | self.categories = [c] + self.categories 179 | 180 | name = property(fget=__getName,fset=__setName) 181 | path = property(fget=__getPath,fset=__setPath) 182 | version = property(fget=__getVersion,fset=setVersion) 183 | author = property(fget=__getAuthor,fset=__setAuthor) 184 | copyright = property(fget=__getCopyright,fset=__setCopyright) 185 | website = property(fget=__getWebsite,fset=__setWebsite) 186 | description = property(fget=__getDescription,fset=__setDescription) 187 | details = property(fget=__getDetails,fset=__setDetails) 188 | # deprecated (>1.9): plugins are not longer associated to a 189 | # single category ! 190 | category = property(fget=__getCategory,fset=__setCategory) 191 | 192 | def _getIsActivated(self): 193 | """ 194 | Return the activated state of the plugin object. 195 | Makes it possible to define a property. 196 | """ 197 | return self.plugin_object.is_activated 198 | 199 | is_activated = property(fget=_getIsActivated) 200 | 201 | def _ensureDetailsDefaultsAreBackwardCompatible(self): 202 | """ 203 | Internal helper function. 204 | """ 205 | if not self.details.has_option("Documentation","Author"): 206 | self.author = "Unknown" 207 | if not self.details.has_option("Documentation","Version"): 208 | self.version = "0.0" 209 | if not self.details.has_option("Documentation","Website"): 210 | self.website = "None" 211 | if not self.details.has_option("Documentation","Copyright"): 212 | self.copyright = "Unknown" 213 | if not self.details.has_option("Documentation","Description"): 214 | self.description = "" 215 | -------------------------------------------------------------------------------- /package/yapsy/PluginManagerDecorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | 4 | """ 5 | Role 6 | ==== 7 | 8 | Provide an easy way to build a chain of decorators extending the 9 | functionalities of the default plugin manager, when it comes to 10 | activating, deactivating or looking into loaded plugins. 11 | 12 | The ``PluginManagerDecorator`` is the base class to be inherited by 13 | each element of the chain of decorator. 14 | 15 | .. warning:: If you want to customise the way the plugins are detected 16 | and loaded, you should not try to do it by implementing a 17 | new ``PluginManagerDecorator``. Instead, you'll have to 18 | reimplement the :doc:`PluginManager` itself. And if you 19 | do so by enforcing the ``PluginManager`` interface, just 20 | giving an instance of your new manager class to the 21 | ``PluginManagerDecorator`` should be transparent to the 22 | "stantard" decorators. 23 | 24 | API 25 | === 26 | """ 27 | 28 | import os 29 | 30 | from yapsy.IPlugin import IPlugin 31 | from yapsy import log 32 | 33 | 34 | class PluginManagerDecorator(object): 35 | """ 36 | Add several responsibilities to a plugin manager object in a 37 | more flexible way than by mere subclassing. This is indeed an 38 | implementation of the Decorator Design Patterns. 39 | 40 | 41 | There is also an additional mechanism that allows for the 42 | automatic creation of the object to be decorated when this object 43 | is an instance of PluginManager (and not an instance of its 44 | subclasses). This way we can keep the plugin managers creation 45 | simple when the user don't want to mix a lot of 'enhancements' on 46 | the base class. 47 | 48 | 49 | About the __init__: 50 | 51 | Mimics the PluginManager's __init__ method and wraps an 52 | instance of this class into this decorator class. 53 | 54 | - *If the decorated_object is not specified*, then we use the 55 | PluginManager class to create the 'base' manager, and to do 56 | so we will use the arguments: ``categories_filter``, 57 | ``directories_list``, and ``plugin_info_ext`` or their 58 | default value if they are not given. 59 | - *If the decorated object is given*, these last arguments are 60 | simply **ignored** ! 61 | 62 | All classes (and especially subclasses of this one) that want 63 | to be a decorator must accept the decorated manager as an 64 | object passed to the init function under the exact keyword 65 | ``decorated_object``. 66 | """ 67 | 68 | def __init__(self, decorated_object=None, 69 | # The following args will only be used if we need to 70 | # create a default PluginManager 71 | categories_filter=None, 72 | directories_list=None, 73 | plugin_info_ext="yapsy-plugin"): 74 | if directories_list is None: 75 | directories_list = [os.path.dirname(__file__)] 76 | if categories_filter is None: 77 | categories_filter = {"Default": IPlugin} 78 | if decorated_object is None: 79 | log.debug("Creating a default PluginManager instance to be decorated.") 80 | from yapsy.PluginManager import PluginManager 81 | decorated_object = PluginManager(categories_filter, 82 | directories_list, 83 | plugin_info_ext) 84 | self._component = decorated_object 85 | 86 | def __getattr__(self,name): 87 | """ 88 | Decorator trick copied from: 89 | http://www.pasteur.fr/formation/infobio/python/ch18s06.html 90 | """ 91 | # print "looking for %s in %s" % (name, self.__class__) 92 | return getattr(self._component,name) 93 | 94 | 95 | def collectPlugins(self): 96 | """ 97 | This function will usually be a shortcut to successively call 98 | ``self.locatePlugins`` and then ``self.loadPlugins`` which are 99 | very likely to be redefined in each new decorator. 100 | 101 | So in order for this to keep on being a "shortcut" and not a 102 | real pain, I'm redefining it here. 103 | """ 104 | self.locatePlugins() 105 | self.loadPlugins() 106 | -------------------------------------------------------------------------------- /package/yapsy/VersionedPluginManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | Role 5 | ==== 6 | 7 | Defines the basic interface for a plugin manager that also keeps track 8 | of versions of plugins 9 | 10 | API 11 | === 12 | """ 13 | 14 | 15 | from packaging.version import Version 16 | 17 | from yapsy.PluginInfo import PluginInfo 18 | from yapsy.IPlugin import IPlugin 19 | from yapsy.PluginManagerDecorator import PluginManagerDecorator 20 | 21 | 22 | class VersionedPluginInfo(PluginInfo): 23 | """ 24 | Gather some info about a plugin such as its name, author, 25 | description... 26 | """ 27 | 28 | def __init__(self, plugin_name, plugin_path): 29 | PluginInfo.__init__(self, plugin_name, plugin_path) 30 | # version number is now required to be a Version object 31 | self.version = Version("0.0") 32 | 33 | def setVersion(self, vstring): 34 | self.version = Version(vstring) 35 | 36 | 37 | class VersionedPluginManager(PluginManagerDecorator): 38 | """ 39 | Handle plugin versioning by making sure that when several 40 | versions are present for a same plugin, only the latest version is 41 | manipulated via the standard methods (eg for activation and 42 | deactivation) 43 | 44 | More precisely, for operations that must be applied on a single 45 | named plugin at a time (``getPluginByName``, 46 | ``activatePluginByName``, ``deactivatePluginByName`` etc) the 47 | targetted plugin will always be the one with the latest version. 48 | 49 | .. note:: The older versions of a given plugin are still reachable 50 | via the ``getPluginsOfCategoryFromAttic`` method. 51 | """ 52 | 53 | def __init__(self, 54 | decorated_manager=None, 55 | categories_filter={"Default":IPlugin}, 56 | directories_list=None, 57 | plugin_info_ext="yapsy-plugin"): 58 | # Create the base decorator class 59 | PluginManagerDecorator.__init__(self,decorated_manager, 60 | categories_filter, 61 | directories_list, 62 | plugin_info_ext) 63 | self.setPluginInfoClass(VersionedPluginInfo) 64 | # prepare the storage for the early version of the plugins, 65 | # for which only the latest version is the one that will be 66 | # kept in the "core" plugin storage. 67 | self._prepareAttic() 68 | 69 | def _prepareAttic(self): 70 | """ 71 | Create and correctly initialize the storage where the wrong 72 | version of the plugins will be stored. 73 | """ 74 | self._attic = {} 75 | for categ in self.getCategories(): 76 | self._attic[categ] = [] 77 | 78 | def setCategoriesFilter(self, categories_filter): 79 | """ 80 | Set the categories of plugins to be looked for as well as the 81 | way to recognise them. 82 | 83 | Note: will also reset the attic toa void inconsistencies. 84 | """ 85 | self._component.setCategoriesFilter(categories_filter) 86 | self._prepareAttic() 87 | 88 | def getLatestPluginsOfCategory(self,category_name): 89 | """ 90 | DEPRECATED(>1.8): Please consider using getPluginsOfCategory 91 | instead. 92 | 93 | Return the list of all plugins belonging to a category. 94 | """ 95 | return self.getPluginsOfCategory(category_name) 96 | 97 | def loadPlugins(self, callback=None, callback_after=None): 98 | """ 99 | Load the candidate plugins that have been identified through a 100 | previous call to locatePlugins. 101 | 102 | In addition to the baseclass functionality, this subclass also 103 | needs to find the latest version of each plugin. 104 | """ 105 | self._prepareAttic() 106 | self._component.loadPlugins(callback, callback_after) 107 | for categ in self.getCategories(): 108 | latest_plugins = {} 109 | allPlugins = self.getPluginsOfCategory(categ) 110 | # identify the latest version of each plugin 111 | for plugin in allPlugins: 112 | name = plugin.name 113 | version = plugin.version 114 | if name in latest_plugins: 115 | if version > latest_plugins[name].version: 116 | older_plugin = latest_plugins[name] 117 | latest_plugins[name] = plugin 118 | self.removePluginFromCategory(older_plugin,categ) 119 | self._attic[categ].append(older_plugin) 120 | else: 121 | self.removePluginFromCategory(plugin,categ) 122 | self._attic[categ].append(plugin) 123 | else: 124 | latest_plugins[name] = plugin 125 | 126 | def getPluginsOfCategoryFromAttic(self,categ): 127 | """ 128 | Access the older version of plugins for which only the latest 129 | version is available through standard methods. 130 | """ 131 | return self._attic[categ] 132 | 133 | -------------------------------------------------------------------------------- /package/yapsy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; python-indent: 4 -*- 2 | 3 | """ 4 | 5 | Overview 6 | ======== 7 | 8 | Yapsy's main purpose is to offer a way to easily design a plugin 9 | system in Python, and motivated by the fact that many other Python 10 | plugin system are either too complicated for a basic use or depend on 11 | a lot of libraries. Yapsy only depends on Python's standard library. 12 | 13 | |yapsy| basically defines two core classes: 14 | 15 | - a fully functional though very simple ``PluginManager`` class 16 | 17 | - an interface ``IPlugin`` which defines the interface of plugin 18 | instances handled by the ``PluginManager`` 19 | 20 | 21 | Getting started 22 | =============== 23 | 24 | The basic classes defined by |yapsy| should work "as is" and enable 25 | you to load and activate your plugins. So that the following code 26 | should get you a fully working plugin management system:: 27 | 28 | from yapsy.PluginManager import PluginManager 29 | 30 | # Build the manager 31 | simplePluginManager = PluginManager() 32 | # Tell it the default place(s) where to find plugins 33 | simplePluginManager.setPluginPlaces(["path/to/myplugins"]) 34 | # Load all plugins 35 | simplePluginManager.collectPlugins() 36 | 37 | # Activate all loaded plugins 38 | for pluginInfo in simplePluginManager.getAllPlugins(): 39 | simplePluginManager.activatePluginByName(pluginInfo.name) 40 | 41 | 42 | .. note:: The ``plugin_info`` object (typically an instance of 43 | ``IPlugin``) plays as *the entry point of each 44 | plugin*. That's also where |yapsy| ceases to guide you: it's 45 | up to you to define what your plugins can do and how you 46 | want to talk to them ! Talking to your plugin will then look 47 | very much like the following:: 48 | 49 | # Trigger 'some action' from the loaded plugins 50 | for pluginInfo in simplePluginManager.getAllPlugins(): 51 | pluginInfo.plugin_object.doSomething(...) 52 | 53 | """ 54 | 55 | __version__="2.0.0" 56 | 57 | # tell epydoc that the documentation is in the reStructuredText format 58 | __docformat__ = "restructuredtext en" 59 | 60 | # provide a default named log for package-wide use 61 | import logging 62 | log = logging.getLogger('yapsy') 63 | 64 | # Some constants concerning the plugins 65 | PLUGIN_NAME_FORBIDEN_STRING=";;" 66 | """ 67 | .. warning:: This string (';;' by default) is forbidden in plugin 68 | names, and will be usable to describe lists of plugins 69 | for instance (see :doc:`ConfigurablePluginManager`) 70 | """ 71 | 72 | import re 73 | RE_NON_ALPHANUM = re.compile(r"\W") 74 | 75 | 76 | def NormalizePluginNameForModuleName(pluginName): 77 | """ 78 | Normalize a plugin name into a safer name for a module name. 79 | 80 | .. note:: may do a little more modifications than strictly 81 | necessary and is not optimized for speed. 82 | """ 83 | if len(pluginName)==0: 84 | return "_" 85 | if pluginName[0].isdigit(): 86 | pluginName = "_" + pluginName 87 | ret = RE_NON_ALPHANUM.sub("_",pluginName) 88 | return ret 89 | -------------------------------------------------------------------------------- /requirements-release.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | setuptools 3 | twine 4 | 5 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | 2 | coverage 3 | pytest 4 | flake8 5 | pytest-cov 6 | -------------------------------------------------------------------------------- /utils/gen_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # Enter release environment 5 | RELEASE_ENV=$( pwd )/$( dirname $0 )/release_env.sh 6 | source $RELEASE_ENV 7 | activate_release_env 8 | 9 | rm -r $PACKAGE_DIR/build 10 | 11 | # Generate doc 12 | python $PACKAGE_DIR/setup.py build_sphinx 13 | 14 | # Exit release environment 15 | deactivate_release_env 16 | -------------------------------------------------------------------------------- /utils/release_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # Designed to be source'd by other scripts. 5 | # Not to be called directly ! 6 | # 7 | # This defines a few useful variables and allows to create a valid 8 | # environment for all "release" related actions of the project. 9 | 10 | 11 | PROJECT_DIR="$( dirname $0 )"/.. 12 | PACKAGE_DIR=$PROJECT_DIR/package 13 | RELEASE_ENV=$PROJECT_DIR/release_env 14 | RELEASE_ENV_REQS=$PROJECT_DIR/requirements-release.txt 15 | 16 | 17 | activate_release_env() { 18 | if [ ! -d $RELEASE_ENV ]; then 19 | echo "Creating a virtual env." && python -m venv $RELEASE_ENV 20 | fi 21 | source $RELEASE_ENV/bin/activate 22 | pip install -U pip 23 | pip install -r $RELEASE_ENV_REQS 24 | } 25 | 26 | deactivate_release_env() { 27 | deactivate 28 | } 29 | -------------------------------------------------------------------------------- /utils/upload_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # Enter release environment 5 | RELEASE_ENV=$( pwd )/$( dirname $0 )/release_env.sh 6 | source $RELEASE_ENV 7 | activate_release_env 8 | 9 | 10 | # Generate package 11 | rm -r $PACKAGE_DIR/build 12 | rm -r $PACKAGE_DIR/dist 13 | python3 $PACKAGE_DIR/setup.py bdist_egg 14 | 15 | # Upload the package 16 | twine upload $PACKAGE_DIR/dist/* 17 | 18 | # Exit relase environment 19 | deactivate_release_env 20 | 21 | --------------------------------------------------------------------------------