{ "info": { "author": "Phillip J. Eby", "author_email": "peak@eby-sarna.com", "bugtrack_url": null, "classifiers": [], "description": "============================================================\nCreating Extensible Applications and Frameworks With Plugins\n============================================================\n\nThe ``peak.util.plugins`` module provides some simple utilities to make it\neasy to create pluggable applications:\n\n* ``Hook`` objects let you easily register and invoke or access extensions that\n have been manually registered or automatically discovered via setuptools\n entry points.\n\n* ``Extensible`` objects can automatically find, load, and activate associated\n add-ons or modifier hooks. This is most useful in conjunction with ``AddOn``\n classes (from the `AddOns`_ package), but can be used with any callables.\n\n* The ``PluginManager`` service manages plugin eggs, and can be subclassed or\n replaced to support alternative means of locating hook implementations.\n (That is, in addition to or in replacement of setuptools.)\n\nThese utilities work well together but can also be used independently. For\nexample, you can use ``Extensible`` objects without using ``Hook`` or\n``PluginManager``, and you can use ``PluginManager`` without ``Hook`` or\n``Extensible``. (Using ``Hook`` objects, however, does require a\n``PluginManager``.)\n\n\n.. _AddOns: http://pypi.python.org/pypi/AddOns/\n\n.. contents:: **Table of Contents**\n\n\nUsing Hooks\n===========\n\nA hook is a place in an application where plugins can add functionality, by\nregistering implementations of the hook. For example, a hook can be used to\nnotify plugins of some application event, or to request plugins' participation\nin some process. Hooks can also just be used to register and find objects\nthat provide some application-specific interface.\n\nHooks are created using a \"group name\", and an optional \"implementation name\".\n(These names correspond to setuptools \"entry point group\" names and \"entry\npoint\" names, respectively.)\n\nThe group name should consist of one or more Python identifiers, separated by\ndots. It does not need to be a valid package or module name, but it should be\na globally unique name. That is, it should include an application or library\nname, so that it can't clash with names in use by other apps, libraries, or\nplugins.\n\nLet's create an example hook::\n\n >>> from peak.util import plugins\n\n >>> hook1 = plugins.Hook('plugins.demo.hook1')\n\n >>> hook1\n Hook('plugins.demo.hook1', None)\n \n >>> list(hook1)\n []\n\nIterating over a hook yields any implementations that have been registered\nunder the hook's group name, but our example hook doesn't have anything\nregistered for it yet. So let's register one, using the ``register`` method::\n\n >>> hook1.register('Hello world')\n >>> list(hook1)\n ['Hello world']\n\nNote that we can pass absolutely any object to ``register()``; hooks do not\nknow or care what sort of objects they operate on. There is also no duplicate\ndetection: if you register an object more than once, it will be listed more\nthan once when you iterate over the hook.\n\nAlso note that hook registration is global and permanent. You cannot remove\na registration once it is added. Also, registration works strictly by group\n*name*, rather than by hook instance. If two or more ``Hook`` objects share\nthe same group name, they will also share the implementations registered for\nthat name::\n\n >>> list(plugins.Hook('plugins.demo.hook1'))\n ['Hello world']\n\nThe way this works is that ``Hook`` objects actually delegate registration and\nretrieval operations to the ``PluginManager`` service. This allows modules to\nregister extensions for other modules to use, without needing the other modules\nto be imported at registration time.\n\n(See also the section below on `The PluginManager Service`_ for more\ninformation.)\n\n\nQuerying and Notifying\n----------------------\n\nIn addition to simply providing iteration over registered implementations,\nhooks have two convenience methods for querying or notifying plugins.\nSpecifically, you can use ``Hook.query()`` and ``Hook.notify()`` to invoke\nhooks whose implementations are functions or other callable objects. For\nexample::\n\n >>> def echo(*args, **kw):\n ... print \"called with\", args, kw\n\n >>> def compute(*args, **kw):\n ... return 42, args, kw\n\n >>> demo = plugins.Hook('plugins.demo.hook2')\n >>> demo.register(echo)\n >>> demo.register(compute)\n\n >>> demo.notify()\n called with () {}\n\n >>> demo.notify(57, x=3)\n called with (57,) {'x': 3}\n\n >>> for result in demo.query(): print result\n called with () {}\n None\n (42, (), {})\n\n >>> for result in demo.query(99): print result\n called with (99,) {}\n None\n (42, (99,), {})\n\nAs you can see, the ``.notify()`` method does not return a value, but simply\ncalls each implementation of the hook with the given arguments. ``.query()``,\nhowever, yields the result of each call.\n\n\nNamed Implementations\n---------------------\n\nBy default, the name of a registered implementation is not significant; most\nof the time, you just want all registered implementations for a specified\ngroup name, regardless of the individual implementations' names. However, it\nis sometimes useful to subdivide a hook's implementations using another level\nof names.\n\nFor example, suppose you are writing a blogging application that processes\nvarious input formats, and you'd like plugins to be able to register hook\nimplementations for specific file extensions. You can do this by registering\nimplementations with an implementation name::\n\n >>> def rst_formatter(filename):\n ... print \"formatting\",filename,\"using reST\"\n\n >>> def txt_formatter(filename):\n ... print \"formatting\",filename,\"as plain text\"\n\n >>> formatters = plugins.Hook('blogtool.formatters')\n >>> formatters.register(rst_formatter, '.rst')\n >>> formatters.register(txt_formatter, '.txt')\n\n >>> list(formatters)\n [, ]\n\nAnd then retriveing them using a ``Hook`` that has both a group name and\nan implementation name::\n\n >>> plugins.Hook('blogtool.formatters', '.rst').notify(\"foo.rst\")\n formatting foo.rst using reST\n\n >>> plugins.Hook('blogtool.formatters', '.txt').notify(\"bar.txt\")\n formatting bar.txt as plain text\n\nThe second argument to the ``Hook()`` constructor (and the ``Hook.register()``\nmethod) is an **implementation name**. If supplied, it must be a non-empty\nASCII string that does not start with a ``[``, contain any control characters\nor ``=`` signs. (It must also not begin or end with whitespace.)\n\nNote, by the way, that these implementation names are not necessarily unique.\nFor example, multiple plugins could register a ``.rst`` formatter, and it is up\nto the code using the ``Hook`` to decide how to handle that! Also notice that\nthe ``blogtool.formatters`` hook above lists *all* registered formatters, no\nmatter what implementation name they are registered under. It's also possible\nfor the same implementation to be registered under more than one name, in which\ncase it will be listed more than once in the overall hook, e.g.::\n\n >>> formatters.register(rst_formatter, '.txt')\n >>> list(formatters)\n [, ,\n ]\n\n >>> list(plugins.Hook('blogtool.formatters', '.txt'))\n [, ]\n\nFinally, note that hooks tied to a specific implementation name can also be\nused for registration, e.g.::\n\n >>> foo = plugins.Hook('blogtool.formatters', '.foo')\n >>> foo\n Hook('blogtool.formatters', '.foo')\n\n >>> foo.register(42)\n >>> list(foo)\n [42]\n\n >>> list(formatters)\n [..., 42]\n\nBut you must either omit the implementation name from the ``.register()`` call\n(as shown above), or else it must match the implementation name the hook was\ncreated with::\n\n >>> foo.register(21, '.foo') # ok - same name\n\n >>> foo.register(99, 'blue!') # not ok!\n Traceback (most recent call last):\n ...\n ValueError: Can only register .foo implementations\n\n \nAutomatic Discovery Using Entry Points and Eggs\n-----------------------------------------------\n\nSo far, we have only seen manually-registered hook implementations. This is\nfine for demonstration, but in a real application with plugins, it would be\nnecessary to first *find* and load the plugins' code in order to do such\nregistrations.\n\nTo address this issue, setuptools provides a feature called \"entry points\",\nwhich can be used to include registration data in plugins' eggs. This allows\nhook implementations to be imported on demand.\n\nTo register a hook implementation for auto-discovery, the plugin's ``setup.py``\nmust use setuptools, and define entry points like this::\n\n setup(\n ...\n entry_points = \"\"\"\n [blogtool.formatters]\n .rst = some.module:some_object\n .txt = other.module:other_object\n\n [some.other.hook]\n any old name here = whatever:SomeClass\n something without an equals sign = foo:bar\n \"\"\"\n )\n\nWhen the plugin is active on ``sys.path`` (i.e., it's importable), iterating\nover an appropriate ``Hook`` object will automatically import the specified\nobject(s) and yield them.\n\nIn the example above, ``some_object`` will be imported from ``some.module``\nand yielded whenever iterating over ``Hook('blogtool.formatters')`` or\n``Hook('blogtool.formatters', '.rst')``. (Note that you must always name the\nimplementations listed in ``setup.py``, even if the application does not look\nup implementations by name!)\n\nThrough this automatic, on-demand import process, it is not necessary to find\nand import the plugins in order to register hook implementations. Instead,\nmerely installing the plugin is sufficient to make its hook implementations\navailable. This also speeds up application startup, because implementations\nare not imported until they are used. (And performance can often be further\nimproved by putting less frequently-used hook implementations into separate\nmodules from those that are used more often.)\n\nOf course, most applications will want to have one or more special directories\nfor installing and using egg plugins; you can use `the PluginManager service`_ to\nlocate and selectively activate the plugins found in such directories.\n\nSee also:\n\n* `The setuptools documentation on entry points`_\n\n* `The pkg_resources entry point API`_\n\n.. _The setuptools documentation on entry points: http://peak.telecommunity.com/DevCenter/setuptools#dynamic-discovery-of-services-and-plugins\n\n.. _The pkg_resources entry point API: http://peak.telecommunity.com/DevCenter/PkgResources#entry-points\n\n\nExtensible Objects\n==================\n\nThe ``Extensible`` mixin class is a convenient way to activate add-on hooks for\nan object. To implement an extensible object, you subclass or mix in\n``Extensible``, add an ``extend_with`` attribute, and call ``load_extensions()``\nat an appropriate time::\n\n >>> AppExtensions = plugins.Hook('my_app.App.extensions')\n\n >>> class App(plugins.Extensible):\n ... extend_with = AppExtensions\n\n >>> def hello(app):\n ... print \"Hi, I'm extending\", app\n >>> AppExtensions.register(hello)\n\n >>> a = App()\n >>> a.load_extensions()\n Hi, I'm extending \n\nThe ``extend_with`` attribute must be a one-argument callable, a sequence of\ncallables, or a sequence of sequences of callables (recursively). Since\n``Hook`` objects are iterable, they can be used as long as their\nimplementations are either one-argument callables or nested sequences thereof.\n\nWhen an ``Extensible`` object's ``load_extensions()`` method is called, the\n``extend_with`` sequence is recursively iterated, and all callables found are\ninvoked with the extensible object as the sole argument. Here's an example\nusing a mixed and nested sequence of callables::\n\n >>> class App(plugins.Extensible):\n ... extend_with = (AppExtensions, (hello, AppExtensions), hello)\n\n >>> a = App()\n >>> a.load_extensions()\n Hi, I'm extending \n Hi, I'm extending \n Hi, I'm extending \n Hi, I'm extending \n\nThe callables can be any 1-argument callable, but you will usually want them\nto be ``AddOn`` classes. Add-ons let you attach additional state and methods\nto an object, in a private namespace that doesn't interfere with the object's\nexisting attributes and methods. (See the `AddOns`_ package for more info.)\n\n\nThe PluginManager Service\n=========================\n\nTODO\n * document ``addEntryPoint()``, ``iterHooks()``\n\n * actually implement some plugin directory services, cached working_set and\n environment, etc.\n\n\nReplacing the PluginManager\n---------------------------\n\nTODO\n * example of subclassing PluginManager and activating it using a with: block\n\n\nThreading Concerns\n------------------\n\nBy default, a separate ``PluginManager`` is created for each thread, and they\nwill share a single working set but use different environments. This is\nprobably NOT what you want in a threaded environment!\n\nThis issue will be addressed in future releases, but for now you should avoid\nusing ``PluginManager`` configuration methods from multiple threads.\n\nIt is, however, safe to use ``Hook`` objects from multiple threads, as this\nis a read-only operation. In principle, accessing a ``Hook`` from one thread\nwhile configuring the ``PluginManager`` in another thread could cause a hook\nto be skipped or doubled, although this is very unlikely. It would be best\nto do all of your ``PluginManager`` configuration before starting threads that\nuse ``Hook`` objects, at least with the current version of ``Plugins``.\n\n\nMailing List\n============\n\nQuestions, discussion, and bug reports for this software should be directed to\nthe PEAK mailing list; see http://www.eby-sarna.com/mailman/listinfo/PEAK/\nfor details.\n\n\nImplementation Status\n=====================\n\nWhile the ``PluginManager`` features are still in development (and remain\nundocumented), this package is only available via SVN checkout. However,\nall documented features are tested and usable, and I don't expect any\nsignificant changes to the APIs currently documented here.", "description_content_type": null, "docs_url": null, "download_url": "svn://svn.eby-sarna.com/svnroot/Plugins#egg=Plugins-dev", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://pypi.python.org/pypi/Plugins", "keywords": null, "license": "PSF or ZPL", "maintainer": null, "maintainer_email": null, "name": "Plugins", "package_url": "https://pypi.org/project/Plugins/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/Plugins/", "project_urls": { "Download": "svn://svn.eby-sarna.com/svnroot/Plugins#egg=Plugins-dev", "Homepage": "http://pypi.python.org/pypi/Plugins" }, "release_url": "https://pypi.org/project/Plugins/0.5a1dev/", "requires_dist": null, "requires_python": null, "summary": "Easily add hooks and plugins to your applications", "version": "0.5a1dev" }, "last_serial": 751271, "releases": { "0.5a1dev": [] }, "urls": [] }