Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
897 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
:mod:`psychopy.plugins` - plugin utilities for extending PsychoPy | ||
================================================================= | ||
|
||
.. automodule:: psychopy.plugins | ||
:members: loadPlugins |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
.. _pluginDevGuide: | ||
|
||
Extending PsychoPy with Plugins | ||
=============================== | ||
|
||
Plugins provide a means for developers to extend PsychoPy, adding new features | ||
and customizations without directly modifying the PsychoPy installation. Read | ||
:ref:`usingplugins` for more information about about plugins before proceeding | ||
on this page. | ||
|
||
How plugins work | ||
---------------- | ||
|
||
The plugin system in PsychoPy functions as a dynamic importer, which imports | ||
additional executable code from plugin packages then patches them into an active | ||
PsychoPy session. This is done by calling the ``psychopy.plugins.loadPlugins()`` | ||
function and passing the names of the desired plugin modules to it. Once | ||
``loadPlugins()`` returns, imported objects are immediately accessible. Any | ||
changes made to PsychoPy with plugins do not persist across sessions, meaning if | ||
Python is restarted, PsychoPy will return to its default behaviour unless | ||
``loadPlugins()`` is called again. While you accomplish the same effect using | ||
conventional ``import`` statements, the plugin loader also automatically handles | ||
patching objects exported by the plugin into PsychoPy's modules and classes. | ||
|
||
To demonstrate why plugins are advantageous over ``import``, let's consider a | ||
case where we want to add support for some display related hardware. This | ||
example requires overriding the default behaviour of the | ||
``psychopy.visual.Window.flip()`` method and adding a new class to | ||
`psychopy.hardware` called ``DisplayDriver``. These objects reside in a package | ||
called `psychopy_display` installed alongside PsychoPy. The following two code | ||
snippets yield the same result: | ||
|
||
Using ``import`` statements:: | ||
|
||
import psychopy | ||
import psychopy.visual as visual | ||
import psychopy.hardware as hardware | ||
import psychopy_display | ||
|
||
visual.Window.flip = psychopy_display.flip | ||
hardware.DisplayDriver = psychopy_display.DisplayDriver | ||
|
||
win = visual.Window() # create a window | ||
hw = hardware.DisplayDriver(win) # initialize our class | ||
|
||
Equivalent to above using a plugin:: | ||
|
||
import psychopy | ||
import psychopy.visual as visual | ||
import psychopy.hardware as hardware | ||
plugins.loadPlugins("psychopy_display") | ||
|
||
win = visual.Window() # create a window | ||
hw = hardware.DisplayDriver(win) | ||
|
||
As demonstrated, using the plugin does not require the user to manually specify | ||
which attributes to assign the imported objects. The plugin loader knows where | ||
to put objects because the modules defines an ``__extends__`` attribute. Other | ||
than the ``__extends__`` statement, the code in `psychopy_display` is exactly | ||
the same in both cases. | ||
|
||
While you could have the module apply patches when imported by doing the | ||
assignments from within the module, the plugin system does some bookkeeping to | ||
keep track of what parts of PsychoPy have been modified, warning the user when | ||
multiple plugins attempt to modify the same attributes. For instance, if another | ||
plugin is loaded and attempts to modify ``psychopy.visual.Window.flip()``, the | ||
plugin system will identify the conflict and inform the user. This safeguards | ||
against possible undefined behaviour arising from the conflict which affects the | ||
operation of previously loaded plugins. | ||
|
||
Plugins can contain executable code which could run when loaded. For instance, | ||
a routine to initialize something so the user doesn't have to explicitly. | ||
|
||
Acceptable Use Policy for Plugins | ||
--------------------------------- | ||
|
||
If one chooses to make available their plugins, regardless of the method of | ||
distribution, they must abide by the following policies for acceptable use. | ||
|
||
1) Plugin developers must be as transparent as possible regarding the intent of | ||
their software. | ||
2) Plugins must not perform actions which may compromise the security or | ||
privacy of the user. | ||
3) The software must not perform unsolicited: file operations (access, | ||
modification, or deletion of files on the users computer or network), changes | ||
to the user's computer hardware or software configuration, or transmission of | ||
data over a network (eg. usage data). | ||
4) Plugins which modify PsychoPy's existing code should be focused on a specific | ||
feature. Avoid creating a plugin which modifies multiple, unrelated aspects | ||
of the software. | ||
|
||
Plugin packages | ||
--------------- | ||
|
||
A plugin has a similar structure to Python package, see the official `Packaging | ||
Python Projects` (https://packaging.python.org/tutorials/packaging-projects) | ||
guide for details. | ||
|
||
To make PsychoPy plugins discernible from any other package in public | ||
repositories, developers should adhere to the official naming convention. Plugin | ||
names should always be prefixed with `psychopy` with individual words separated | ||
with a `-` or `_` symbol (i.e. `psychopy-quest-procedure`). What you name the | ||
package is up to you, but keep it concise and informative. The `name` argument | ||
of the `setup()` function in `setup.py` file should be set to the name you've | ||
chosen. Furthermore, the package directory or module file the plugin code | ||
resides in should be named the same, but with `_` underscores separating words | ||
(i.e. `psychopy_quest_procedure`). This convention is used to make plugin | ||
packages easier to find once installed locally. | ||
|
||
Below is an example of what a package's directory structure should look like: | ||
|
||
```TODO``` | ||
|
||
The `__init__.py` in the sub-directory is the entry point for your plugin code. | ||
|
||
The ``__extends__`` statement | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The ``__extends__`` module attribute is **required** by all PsychoPy plugins. If | ||
``__extends__`` is not defined in file used as the entry point for plugin | ||
module, it cannot be loaded by ``loadPlugins()``. The plugin loader imports the | ||
module and looks for this attribute to not only identify whether a module is a | ||
plugin, but to determine where to assign objects within PsychoPy. | ||
|
||
The value of ``__extends__`` is always either a dictionary or `None`. Dictionary | ||
keys are strings specifying the *fully qualified name* of a PsychoPy object | ||
whose attribute you wish to extend or modify. Target PsychoPy objects can be | ||
modules (eg. `psychopy.visual`), classes (eg. `psychopy.visual.Window`) and | ||
their methods. | ||
|
||
Note that objects can only be assigned to unbound classes and their methods and | ||
will not modify instances present before the plugin was loaded. The items of the | ||
``__extends__`` dictionary are strings or lists of strings specifying the names | ||
of objects to place in the associated namespace. For example, an ``__extends__`` | ||
statement may look like this:: | ||
|
||
__extends__ = {'psychopy.core': "MyTimer", | ||
'psychopy.visual': ["MyStimClass", "myFunc"], | ||
'psychopy.visual.Window': "flip"} | ||
|
||
Where `"MyTimer"`, `"MyStimClass"`, `"flip"` and `"myFunc"` are objects defined | ||
in the scope of the plugin module. When the plugin is loaded, `MyTimer` will be | ||
placed in `psychopy.core`, and `MyStimClass` and `myFunc` in `psychopy.visual`. | ||
The method ``psychopy.visual.Window.flip()`` will be replaced with `flip`. | ||
After a plugin is loaded, the behaviour of ``flip()`` will change and all other | ||
object will be accessible within their respective scopes (eg. | ||
``psychopy.visual.myFunc()`` will be callable). | ||
|
||
In a some cases a plugin may not extend any namespaces, but still contains code | ||
to modify PsychoPy. This is the case for plugins which alters the Builder | ||
interface (eg. add a menu item). If so, the file must still contain a | ||
``__extends__`` directive but it may be set to `None` or an empty dictionary. | ||
|
||
Note the optional ``__all__`` attribute some modules define is ignored by the | ||
plugin loader. You can include ``__all__`` in your plugin module if you wish to | ||
allow it to be imported conventionally, or to expose non-PsychoPy related | ||
objects. | ||
|
||
Optional ``__load()`` and ``__shutdown__()`` functions | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Some plugins may need to execute code when loaded or to clean up when PsychoPy | ||
closes. You can indicate which code to run in either of these events by defining | ||
optional ``__load()`` and ``__shutdown()`` functions in the same file | ||
``__extends__`` is defined. If present, the ``__load()`` function is called | ||
before assigning objects specified by ``__extends__`` and ``__shutdown()`` is | ||
called when ``psychopy.core.quit()`` is invoked. | ||
|
||
Coding and documentation style | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Since plugins are not part of PsychoPy, developers are not compelled to adhere | ||
to the official style guide. However, to provide a consistent experience for | ||
users, it is highly recommended that any user facing objects exported by the | ||
plugin do use the official style conventions. See :ref:`demostyleguide` for more | ||
information. For documentation, PsychoPy standardized on the `NumpyDoc` style | ||
for new code. | ||
|
||
Creating a plugin example | ||
------------------------- | ||
|
||
This example will demonstrate how to create and package a plugin for | ||
distribution. Here we would like to add a new stimulus class and function to | ||
`psychopy.visual` called `MyStim` and `helperFunc`, respectively. | ||
|
||
Setting up project files | ||
~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The source tree of the plugin resembles a typical Python package. The top-level | ||
project directory is named `psychopy_mystim`, in it we have files `setup.py`, | ||
`README.md`, and `LICENCE`, and module sub-directory named `psychopy_mystim` | ||
with a `__init__.py` file inside it. This sub-directory defines the entry | ||
point for the plugin. | ||
|
||
Below is a diagram of what the project directory should look like when viewed | ||
in a file manager: | ||
|
||
``example`` | ||
|
||
Configuring `setup.py` | ||
~~~~~~~~~~~~~~~~~~~~~~ | ||
``TODO`` | ||
|
||
Adding code | ||
~~~~~~~~~~~ | ||
|
||
The Python file serving as the entry point for your package needs to define an | ||
``__extends__`` statement which indicates which objects need to be placed into | ||
which namespace. For our example, we want to put objects ``MyStim`` and | ||
``helperFunc`` into `psychopy.visual`. Therefore our ``__extends__`` statement | ||
should be placed in the `__init__.py` file in our module sub-directory and | ||
defined as:: | ||
|
||
__extends__ = {'psychopy.visual': ["MyStim", "helperFunc"]} | ||
|
||
Optionally, we can also define an ``__all__`` statement to handle the case where | ||
we import the plugin module directly (note that PsychoPy plugins must *always* | ||
define ``__extends__`` even if ``__all__`` is present):: | ||
|
||
__all__ = ["MyStim", "helperFunc"] | ||
|
||
Now we add our ``import`` statements. ``MyStim`` is a subclass of | ||
``BaseShapeStim`` so we need to import it:: | ||
|
||
import psychopy | ||
from psychopy.visual.shape import BaseShapeStim | ||
|
||
You can also add additional import statements to bring in objects from other | ||
files located in the module sub-directory. In our example, ``helperFunc`` is | ||
defined in the file ``tools.py`` and we would like to make it exportable. To do | ||
this, we add add an additional import statement which brings the function into | ||
the module namespace:: | ||
|
||
import psychopy | ||
from psychopy.visual.shape import BaseShapeStim | ||
from psychopy_mystim.tools import myFunc | ||
|
||
We can now define our ``MyStim`` class which may look something like this:: | ||
|
||
class MyStim(BaseShapeStim): | ||
def __init__(*args, **kwargs): | ||
pass | ||
|
||
Packaging and testing | ||
~~~~~~~~~~~~~~~~~~~~~ | ||
``TODO`` | ||
|
||
|
||
Plugins as patches | ||
------------------ | ||
|
||
Plugins can also be used to install and distribute unofficial patches or | ||
hotfixes to quickly fix bugs in current releases of PsychoPy without needing to | ||
manually edit files in your existing PsychoPy installation. This also allows for | ||
fixes to be applied across several installations too. | ||
|
||
Note that not all features in PsychoPy can be patched and will require upstream | ||
fixes. In any case make sure you report the bug to the developers! | ||
|
||
Example patch | ||
~~~~~~~~~~~~~ | ||
``TODO`` | ||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ Further information: | |
recipes/recipes | ||
faqs/faqs | ||
resources/resources | ||
usingplugins | ||
|
||
For developers: | ||
|
||
|
Oops, something went wrong.