Skip to content

Commit

Permalink
Merge pull request #590 from astrofrog/plugin-loading-registry
Browse files Browse the repository at this point in the history
Improve loading of plugins
  • Loading branch information
astrofrog committed Mar 19, 2015
2 parents c10e481 + 238241d commit 6343c55
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 56 deletions.
1 change: 0 additions & 1 deletion doc/gui_guide/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ To obtain a fresh ``config.py`` file to edit, run the command line program::

Which will create a new file at ``~/.glue/config.py``


Example Usage: Custom Link Functions
------------------------------------

Expand Down
150 changes: 123 additions & 27 deletions doc/python_guide/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,48 @@

Customizing Glue
================
There are a few ways to customize the Glue UI
with configuration files and plugins.

There are a few ways to customize the Glue UI with configuration files and
plugins.

Configuration Files
-------------------

Each time Glue starts, it looks for and executes a configuration file. This is a normal python script into which users can define or import new functions to link data, plug in their own visualization modules, set up logging, etc.
Each time Glue starts, it looks for and executes a configuration file. This
is a normal python script into which users can define or import new functions
to link data, plug in their own visualization modules, set up logging, etc.

The glue configuration file is called ``config.py``. Glue looks for this file in the following locations, in order:
The glue configuration file is called ``config.py``. Glue looks for this file
in the following locations, in order:

* The current working directory
* The path specified in the ``GLUERC`` environment variable, if present
* The path ``.glue/config.py`` within the user's home directory

Registries
----------

Glue is written so as to allow users to easily register new plug-in data
viewers, tools, exporters, and more. Registering such plug-ins can be done
via *registries* located in the ``glue.config`` sub-package. Registries
include for example ``link_function``, ``data_factory``, ``colormaps``, and
so on. As demonstrated below, some registries can be used as decorators (see
e.g. `Adding Custom Link Functions`_) and for others you can add items using
the ``add`` method (see e.g. `Custom Colormaps`_).

In the following sections, we show a few examples of registering new
functionality, and a full list of available registries is given in `Complete
list of registries`_.

Adding Custom Link Functions
----------------------------
.. _custom_links:

From the :ref:`Link Data Dialog <getting_started_link>`, you inform Glue how to convert between quantities among different data sets. You do this by selecting a translation function, and specifying which data attributes should be treated as inputs and outputs. You can use the configuration file to specify custom translation functions. Here's how:
From the :ref:`Link Data Dialog <getting_started_link>`, you inform Glue how
to convert between quantities among different data sets. You do this by
selecting a translation function, and specifying which data attributes should
be treated as inputs and outputs. You can use the configuration file to
specify custom translation functions. Here's how:

.. literalinclude:: scripts/config_link_example.py

Expand All @@ -30,14 +52,15 @@ Some remarks about this code:
#. We provide a short summary of the function in the ``info`` keyword, and a list of ``output_labels``. Usually, only one quantity is returned, so ``output_labels`` has one element.
#. Glue will always pass numpy arrays as inputs to a link function, and expects a numpy array (or a tuple of numpy arrays) as output

With this code in your configuration file, the ``deg_to_rad`` function is available in the ``Link Data`` dialog:
With this code in your configuration file, the ``deg_to_rad`` function is
available in the ``Link Data`` dialog:

.. figure:: images/custom_link.png
:align: center
:width: 200px

This would allow you to link between two datasets with different conventions for specifying angles.

This would allow you to link between two datasets with different conventions
for specifying angles.

Custom Data Loaders
-------------------
Expand All @@ -46,10 +69,10 @@ Custom Data Loaders
Glue lets you create custom data loader functions,
to use from within the GUI.

Here's a quick example: the default image loader in Glue
reads each color in an RGB image into 3 two-dimensional components.
Perhaps you want to be able to load these images into a single 3-dimensional
component called ``cube``. Here's how you could do this::
Here's a quick example: the default image loader in Glue reads each color in
an RGB image into 3 two-dimensional components. Perhaps you want to be able
to load these images into a single 3-dimensional component called ``cube``.
Here's how you could do this::

from glue.config import data_factory
from glue.core import Data
Expand All @@ -68,12 +91,14 @@ Let's look at this line-by-line:
* The `is_jpeg` function takes a filename and keywords as input,
and returns True if a data factory can handle this file

* The ``@data_factory`` decorator is how Glue "finds" this function.
Its two arguments are a label, and the `is_jpeg` identifier function
* The ``@data_factory`` decorator is how Glue "finds" this function. Its two
arguments are a label, and the `is_jpeg` identifier function

* The first line in ``read_jpeg`` uses scikit-image to load an image file into a NumPy array.
* The first line in ``read_jpeg`` uses scikit-image to load an image file
into a NumPy array.

* The second line :ref:`constructs a Data object <data_creation>` from this array, and returns the result.
* The second line :ref:`constructs a Data object <data_creation>` from this
array, and returns the result.

If you put this in your ``config.py`` file, you will see a new
file type when loading data:
Expand All @@ -82,24 +107,27 @@ file type when loading data:
:align: center
:width: 50%

If you open a file using this file type selection, Glue will pass
the path of this file to your function, and use the resulting Data
object.
If you open a file using this file type selection, Glue will pass the path of
this file to your function, and use the resulting Data object.

For more examples of custom data loaders, see the `example repository <https://github.com/glue-viz/glue-data-loaders>`_.
For more examples of custom data loaders, see the `example repository
<https://github.com/glue-viz/glue-data-loaders>`_.

Custom Colormaps
----------------
You can add additional matplotlib colormaps to Glue's image viewer by adding the following code into ``config.py``::

You can add additional matplotlib colormaps to Glue's image viewer by adding
the following code into ``config.py``::

from glue.config import colormaps
from matplotlib.cm import Paired
colormaps.add('Paired', Paired)

Custom Subset Actions
---------------------
You can add menu items to run custom functions on subsets.
Use the following pattern in ``config..py``::

You can add menu items to run custom functions on subsets. Use the following
pattern in ``config..py``::

from glue.config import single_subset_action

Expand All @@ -108,7 +136,75 @@ Use the following pattern in ``config..py``::

single_subset_action('Menu title', callback)

This menu item is available by right clicking on a subset
when a single subset is selected in the Data Collection window. Note
that you must select the subset specific to a particular Data set,
and not the parent Subset Group.
This menu item is available by right clicking on a subset when a single
subset is selected in the Data Collection window. Note that you must select
the subset specific to a particular Data set, and not the parent Subset Group.

Complete list of registries
---------------------------

A few registries have been demonstrated above, and a complete list of main
registries are listed below. All can be imported from ``glue.config`` - each
registry is an instance of a class, given in the second column, and which
provides more information about what the registry is and how it can be used.

========================== =======================================================
Registry name Registry class
========================== =======================================================
``qt_client`` :class:`glue.config.QtClientRegistry`
``tool_registry`` :class:`glue.config.QtToolRegistry`
``data_factory`` :class:`glue.config.DataFactoryRegistry`
``link_function`` :class:`glue.config.LinkFunctionRegistry`
``link_helper`` :class:`glue.config.LinkHelperRegistry`
``colormaps`` :class:`glue.config.ColormapRegistry`
``exporters`` :class:`glue.config.ExporterRegistry`
``settings`` :class:`glue.config.SettingRegistry`
``fit_plugin`` :class:`glue.config.ProfileFitterRegistry`
``single_subset_action`` :class:`glue.config.SingleSubsetLayerActionRegistry`
========================== =======================================================

Deferring loading of plug-in functionality (advanced)
-----------------------------------------------------

In some cases, you may want to defer the loading of your plugin until it is
actually needed. To do this:

* Place the code for your plugin in a file or package that could be imported
from the ``config.py`` (but don't import it directly - it just has to be
importable)

* Include a function called ``setup`` alongside the plugin, and this function
should contain code to actually add your custom tools to the appropriate
registries.

* In ``config.py``, you can then add the plugin file or package to a registry
by using the ``lazy_add`` method and pass a string giving the name of the
package or sub-package containing the plugin.

Imagine that you have created a data viewer ``MyQtViewer``. You could
directly register it using::

from glue.config import qt_client
qt_client.add(MyQtViewer)

but if you want to defer the loading of the ``MyQtViewer`` class, you can
place the definition of ``MyQtViewer`` in a file called e.g.
``my_qt_viewer.py`` that is located in the same directory as your
``config.py`` file. This file should look something like::

class MyQtViewer(...):
...

def setup():
from glue.config import qt_client
qt_client.add(MyQtViewer)

then in ``config.py``, you can do::

from glue.config import qt_client
qt_client.lazy_add('my_qt_viewer')

With this in place, the ``setup`` in your plugin will only get called if the
Qt data viewers are needed, but you will avoid unecessarily importing Qt if
you only want to access ``glue.core``.

5 changes: 4 additions & 1 deletion glue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,16 @@ def make_selector_func(roi):
from .qt.custom_viewer import CustomViewer
return CustomViewer.create_new_subclass(name, **kwargs)

# Register default plugins (but don't load them)
from .plugins import register_plugins
register_plugins()

# Load user's configuration file
from .config import load_configuration
env = load_configuration()

from .qglue import qglue


from .version import __version__

def test(no_optional_skip=False):
Expand Down
27 changes: 22 additions & 5 deletions glue/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@

__all__ = ['Registry', 'SettingRegistry', 'ExporterRegistry',
'ColormapRegistry', 'DataFactoryRegistry', 'QtClientRegistry',
'LinkFunctionRegistry', 'LinkHelperRegistry',
'ProfileFitterRegistry',
'LinkFunctionRegistry', 'LinkHelperRegistry', 'QtToolRegistry',
'SingleSubsetLayerActionRegistry', 'ProfileFitterRegistry',
'qt_client', 'data_factory', 'link_function', 'link_helper',
'colormaps',
'exporters', 'settings', 'fit_plugin', 'auto_refresh']
'colormaps', 'exporters', 'settings', 'fit_plugin', 'auto_refresh']


class Registry(object):
Expand All @@ -35,13 +34,15 @@ class Registry(object):

def __init__(self):
self._members = []
self._lazy_members = []
self._loaded = False

@property
def members(self):
""" A list of the members in the registry.
The return value is a list. The contents of the list
are specified in each subclass"""
self._load_lazy_members()
if not self._loaded:
self._members = self.default_members() + self._members
self._loaded = True
Expand All @@ -54,9 +55,23 @@ def default_members(self):
return []

def add(self, value):
""" Add a new item to the registry """
"""
Add a new item to the registry.
"""
self._members.append(value)

def lazy_add(self, value):
"""
Add a reference to a plugin which will be loaded when needed.
"""
self._lazy_members.append(value)

def _load_lazy_members(self):
from .plugins import load_plugin
while self._lazy_members:
plugin = self._lazy_members.pop()
load_plugin(plugin)

def __iter__(self):
return iter(self.members)

Expand Down Expand Up @@ -234,10 +249,12 @@ class QtToolRegistry(Registry):

def __init__(self):
self._members = {}
self._lazy_members = []
self._loaded = False

@property
def members(self):
self._load_lazy_members()
if not self._loaded:
defaults = self.default_members()
for key in defaults:
Expand Down
29 changes: 21 additions & 8 deletions glue/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
from . import export_d3po
from . import export_plotly
from . import ginga_viewer
import sys


def load_all_plugins():
def register_plugins():
from ..config import qt_client, exporters
qt_client.lazy_add('glue.plugins.ginga_viewer')
exporters.lazy_add('glue.plugins.export_d3po')
exporters.lazy_add('glue.plugins.export_plotly')
exporters.lazy_add('glue.plugins.tools.pv_slicer')
exporters.lazy_add('glue.plugins.tools.spectrum_tool')


def load_plugin(plugin):
"""
Load built-in plugins
Load plugin referred to by name 'plugin'
"""

from .ginga_viewer import load_ginga_viewer_plugin
load_ginga_viewer_plugin()
# When Python 2.6 is no longer supported, we can use:
# import importlib
# module = importlib.import_module(plugin)
__import__(plugin)
module = sys.modules[plugin]
if hasattr(module, 'setup'):
module.setup()
else:
raise AttributeError("Plugin {0} should define 'setup' function".format(plugin))
7 changes: 5 additions & 2 deletions glue/plugins/export_d3po.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json
import os

from ..config import exporters
from ..qt.widgets import ScatterWidget, HistogramWidget
from ..core import Subset

Expand Down Expand Up @@ -247,7 +246,11 @@ def launch(path):
webbrowser.open('http://0.0.0.0:%i' % PORT)


exporters.add('D3PO', save_d3po, can_save_d3po, outmode='directory')
def setup():
from ..logger import logger
from ..config import exporters
exporters.add('D3PO', save_d3po, can_save_d3po, outmode='directory')
logger.info("Loaded d3po exporter plugin")


HTML = """
Expand Down
11 changes: 7 additions & 4 deletions glue/plugins/export_plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
except ImportError:
plotly = None

from ..config import exporters, settings
from ..qt.widgets import ScatterWidget, HistogramWidget
from ..core.data import CategoricalComponent
from ..core.layout import Rectangle, snap_to_grid
Expand Down Expand Up @@ -305,6 +304,10 @@ def save_plotly(application, label):
plotly.sign_in(user, apikey)
plotly.plot(*args, **kwargs)

exporters.add('Plotly', save_plotly, can_save_plotly, outmode='label')
settings.add('PLOTLY_USER', 'Glue')
settings.add('PLOTLY_APIKEY', 't24aweai14')
def setup():
from ..logger import logger
from ..config import exporters, settings
exporters.add('Plotly', save_plotly, can_save_plotly, outmode='label')
settings.add('PLOTLY_USER', 'Glue')
settings.add('PLOTLY_APIKEY', 't24aweai14')
logger.info("Loaded plotly exporter plugin")
Loading

0 comments on commit 6343c55

Please sign in to comment.