diff --git a/config/plugins/visualizations/graphview/config/graphview.xml b/config/plugins/visualizations/graphview/config/graphview.xml
index c809a317ae3..836e60da855 100644
--- a/config/plugins/visualizations/graphview/config/graphview.xml
+++ b/config/plugins/visualizations/graphview/config/graphview.xml
@@ -16,5 +16,5 @@
dataset_id
- graphview/templates/graphview.mako
+ graphview.mako
diff --git a/config/plugins/visualizations/scatterplot/config/scatterplot.xml b/config/plugins/visualizations/scatterplot/config/scatterplot.xml
index 5942fc99b90..d206f46b2a1 100644
--- a/config/plugins/visualizations/scatterplot/config/scatterplot.xml
+++ b/config/plugins/visualizations/scatterplot/config/scatterplot.xml
@@ -11,5 +11,5 @@
dataset_id
- scatterplot/templates/scatterplot.mako
+ scatterplot.mako
diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py
index 378cffe3e92..ad477e065f9 100644
--- a/lib/galaxy/app.py
+++ b/lib/galaxy/app.py
@@ -22,6 +22,8 @@ from galaxy.sample_tracking import external_service_types
from galaxy.openid.providers import OpenIDProviders
from galaxy.tools.data_manager.manager import DataManagers
+from galaxy.web.base import pluginframework
+
import logging
log = logging.getLogger( __name__ )
@@ -123,8 +125,11 @@ class UniverseApplication( object ):
# Load genome indexer tool.
load_genome_index_tools( self.toolbox )
# visualizations registry: associates resources with visualizations, controls how to render
- self.visualizations_registry = VisualizationsRegistry.from_config(
- self.config.visualizations_plugins_directory, self.config )
+ self.visualizations_registry = None
+ if self.config.visualizations_plugins_directory:
+ self.visualizations_registry = VisualizationsRegistry( self,
+ directories_setting=self.config.visualizations_plugins_directory,
+ template_cache_dir=self.config.template_cache )
# Load security policy.
self.security_agent = self.model.security_agent
self.host_security_agent = galaxy.security.HostAgent( model=self.security_agent.model, permitted_actions=self.security_agent.permitted_actions )
diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py
index d2701555a8f..9a6506be36d 100644
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -294,9 +294,7 @@ class Configuration( object ):
self.fluent_log = string_as_bool( kwargs.get( 'fluent_log', False ) )
self.fluent_host = kwargs.get( 'fluent_host', 'localhost' )
self.fluent_port = int( kwargs.get( 'fluent_port', 24224 ) )
- # PLUGINS:
- self.plugin_frameworks = []
- # visualization framework
+ # visualization plugin framework
self.visualizations_plugins_directory = kwargs.get( 'visualizations_plugins_directory', None )
@property
diff --git a/lib/galaxy/visualization/registry.py b/lib/galaxy/visualization/registry.py
index b25a5fedf1b..375a96b99b3 100644
--- a/lib/galaxy/visualization/registry.py
+++ b/lib/galaxy/visualization/registry.py
@@ -39,9 +39,10 @@ user_pref for ordering/ex/inclusion of particular visualizations
"""
# ------------------------------------------------------------------- the registry
-class VisualizationsRegistry( pluginframework.PluginFramework ):
+class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
"""
Main responsibilities are:
+ - discovering visualization plugins in the filesystem
- testing if an object has a visualization that can be applied to it
- generating a link to controllers.visualization.render with
the appropriate params
@@ -58,31 +59,79 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
'sweepster',
'phyloviz'
]
- #: directories under plugin_directory that aren't plugins
- non_plugin_directories = []
- def __init__( self, registry_filepath, template_cache_dir ):
- super( VisualizationsRegistry, self ).__init__( registry_filepath, 'visualizations', template_cache_dir )
+ def __str__( self ):
+ return self.__class__.__name__
+ def __init__( self, app, **kwargs ):
+ self.config_parser = VisualizationsConfigParser()
+ super( VisualizationsRegistry, self ).__init__( app, 'visualizations', **kwargs )
# what to use to parse query strings into resources/vars for the template
self.resource_parser = ResourceParser()
log.debug( '%s loaded', str( self ) )
- def load_configuration( self ):
+ def is_plugin( self, plugin_path ):
"""
- Builds the registry by parsing the `config/*.xml` files for every plugin
- in ``get_plugin_directories`` and stores the results in ``self.listings``.
+ Determines whether the given filesystem path contains a plugin.
- ..note::
- This could be used to re-load a new configuration without restarting
- the instance.
+ In this base class, all sub-directories are considered plugins.
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the
+ potential plugin
+ :rtype: bool
+ :returns: True if the path contains a plugin
"""
- try:
- self.listings = VisualizationsConfigParser.parse( self.get_plugin_directories() )
+ # plugin_path must be a directory, have a config dir
+ if not os.path.isdir( plugin_path ):
+ return False
+ if not 'config' in os.listdir( plugin_path ):
+ return False
+ expected_config_filename = '%s.xml' %( os.path.split( plugin_path )[1] )
+ if not os.path.isfile( os.path.join( plugin_path, 'config', expected_config_filename ) ):
+ return False
+ return True
- except Exception, exc:
- log.exception( 'Error parsing visualizations plugins %s', self.plugin_directories )
- raise
+ def load_plugin( self, plugin_path ):
+ """
+ Create the visualization plugin object, parse its configuration file,
+ and return it.
+
+ Plugin bunches are decorated with:
+ * config_file : the path to this visualization's config file
+ * config : the parsed configuration for this visualization
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the plugin
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ #TODO: possibly move this after the config parsing to allow config to override?
+ plugin = super( VisualizationsRegistry, self ).load_plugin( plugin_path )
+
+ # config file is required, otherwise skip this visualization
+ plugin[ 'config_file' ] = os.path.join( plugin_path, 'config', ( plugin.name + '.xml' ) )
+ config = self.config_parser.parse_file( plugin.config_file )
+ if not config:
+ return None
+ plugin[ 'config' ] = config
+
+ return plugin
+
+ # -- getting resources for visualization templates from link query strings --
+ # -- building links to visualizations from objects --
+ def get_visualizations( self, trans, target_object ):
+ """
+ Get the names of visualizations usable on the `target_object` and
+ the urls to call in order to render the visualizations.
+ """
+ #TODO:?? a list of objects? YAGNI?
+ applicable_visualizations = []
+ for vis_name in self.plugins:
+ url_data = self.get_visualization( trans, vis_name, target_object )
+ if url_data:
+ applicable_visualizations.append( url_data )
+ return applicable_visualizations
def get_visualization( self, trans, visualization_name, target_object ):
"""
@@ -90,12 +139,11 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
`visualization_name` if it's applicable to `target_object` or
`None` if it's not.
"""
- # a little weird to pass trans because this registry is part of the trans.app
- listing_data = self.listings.get( visualization_name, None )
- if not listing_data:
+ visualization = self.plugins.get( visualization_name, None )
+ if not visualization:
return None
- data_sources = listing_data[ 'data_sources' ]
+ data_sources = visualization.config[ 'data_sources' ]
for data_source in data_sources:
# currently a model class is required
model_class = data_source[ 'model_class' ]
@@ -109,11 +157,11 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
param_data = data_source[ 'to_params' ]
url = self.get_visualization_url( trans, target_object, visualization_name, param_data )
- link_text = listing_data.get( 'link_text', None )
+ link_text = visualization.config.get( 'link_text', None )
if not link_text:
# default to visualization name, titlecase, and replace underscores
link_text = visualization_name.title().replace( '_', ' ' )
- render_location = listing_data.get( 'render_location' )
+ render_location = visualization.config.get( 'render_location' )
# remap some of these vars for direct use in ui.js, PopupMenu (e.g. text->html)
return {
'href' : url,
@@ -123,25 +171,12 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
return None
- # -- building links to visualizations from objects --
- def get_visualizations( self, trans, target_object ):
- """
- Get the names of visualizations usable on the `target_object` and
- the urls to call in order to render the visualizations.
- """
- #TODO:?? a list of objects? YAGNI?
- applicable_visualizations = []
- for vis_name in self.listings:
- url_data = self.get_visualization( trans, vis_name, target_object )
- if url_data:
- applicable_visualizations.append( url_data )
- return applicable_visualizations
-
def is_object_applicable( self, trans, target_object, data_source_tests ):
"""
Run a visualization's data_source tests to find out if
it be applied to the target_object.
"""
+ #log.debug( 'is_object_applicable( self, trans, %s, %s )', target_object, data_source_tests )
for test in data_source_tests:
test_type = test[ 'type' ]
result_type = test[ 'result_type' ]
@@ -164,7 +199,6 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
if test_fn( target_object, test_result ):
#log.debug( 'test passed' )
return True
-
return False
def get_visualization_url( self, trans, target_object, visualization_name, param_data ):
@@ -219,9 +253,9 @@ class VisualizationsRegistry( pluginframework.PluginFramework ):
Both `params` and `param_modifiers` default to an empty dictionary.
"""
- visualization = self.listings.get( visualization_name )
- expected_params = visualization.get( 'params', {} )
- param_modifiers = visualization.get( 'param_modifiers', {} )
+ visualization = self.plugins.get( visualization_name )
+ expected_params = visualization.config.get( 'params', {} )
+ param_modifiers = visualization.config.get( 'param_modifiers', {} )
return ( expected_params, param_modifiers )
def query_dict_to_resources( self, trans, controller, visualization_name, query_dict ):
@@ -259,13 +293,6 @@ class VisualizationsConfigParser( object ):
"""
VALID_RENDER_LOCATIONS = [ 'galaxy_main', '_top', '_blank' ]
- @classmethod
- def parse( cls, plugin_directories, debug=False ):
- """
- Static class interface.
- """
- return cls( debug ).parse_plugins( plugin_directories )
-
def __init__( self, debug=False ):
self.debug = debug
@@ -274,58 +301,19 @@ class VisualizationsConfigParser( object ):
self.param_parser = ParamParser()
self.param_modifier_parser = ParamModifierParser()
- def parse_plugins( self, plugin_directories ):
- """
- Parses the config files for each plugin sub-dir in `base_path`.
-
- :param plugin_directories: a list of paths to enabled plugins.
-
- :returns: registry data in dictionary form
- """
- returned = {}
- for plugin_path in plugin_directories:
- returned.update( self.parse_plugin( plugin_path ) )
- return returned
-
- def parse_plugin( self, plugin_path ):
- """
- Parses any XML files in ``/config``.
-
- If an error occurs while parsing a visualizations entry, it is skipped.
- :returns: registry data in dictionary form
- ..note::
- assumes config files are in a 'config' sub-dir of each plugin
- """
- returned = {}
-
- plugin_config_path = os.path.join( plugin_path, 'config' )
- if not os.path.isdir( plugin_config_path ):
- return returned
-
- for xml_filepath in glob.glob( os.path.join( plugin_config_path, '*.xml' ) ):
- try:
- visualization_name, visualization = self.parse_file( xml_filepath )
- # skip vis' with parsing errors - don't shutdown the startup
- except ParsingException, parse_exc:
- log.error( 'Skipped visualization config "%s" due to parsing errors: %s',
- xml_filepath, str( parse_exc ), exc_info=self.debug )
-
- if visualization:
- returned[ visualization_name ] = visualization
- log.debug( 'Visualization config loaded for: %s', visualization_name )
-
- return returned
-
def parse_file( self, xml_filepath ):
"""
Parse the given XML file for visualizations data.
:returns: tuple of ( `visualization_name`, `visualization` )
"""
- xml_tree = galaxy.util.parse_xml( xml_filepath )
- visualization_conf = xml_tree.getroot()
- visualization_name = visualization_conf.get( 'name' )
- visualization = self.parse_visualization( visualization_conf )
- return visualization_name, visualization
+ try:
+ xml_tree = galaxy.util.parse_xml( xml_filepath )
+ visualization = self.parse_visualization( xml_tree.getroot() )
+ return visualization
+ # skip vis' with parsing errors - don't shutdown the startup
+ except ParsingException, parse_exc:
+ log.exception( 'Skipped visualization config "%s" due to parsing errors', xml_filepath )
+ return None
def parse_visualization( self, xml_tree ):
"""
diff --git a/lib/galaxy/web/base/pluginframework.py b/lib/galaxy/web/base/pluginframework.py
index 6d75fdc48bf..a726dc7d78f 100644
--- a/lib/galaxy/web/base/pluginframework.py
+++ b/lib/galaxy/web/base/pluginframework.py
@@ -1,38 +1,315 @@
"""
Base class for plugins - frameworks or systems that may:
+ * add code at startup
+ * allow hooks to be called
+and base class for plugins that:
* serve static content
* serve templated html
* have some configuration at startup
"""
import os.path
-import glob
import sys
+import imp
import pkg_resources
pkg_resources.require( 'MarkupSafe' )
pkg_resources.require( 'Mako' )
import mako
-from galaxy.util import listify
+from galaxy import util
+from galaxy.util import odict
+from galaxy.util import bunch
import logging
log = logging.getLogger( __name__ )
# ============================================================================= exceptions
-class PluginFrameworkException( Exception ):
+class PluginManagerException( Exception ):
"""Base exception for plugin frameworks.
"""
pass
-class PluginFrameworkConfigException( PluginFrameworkException ):
+class PluginManagerConfigException( PluginManagerException ):
"""Exception for plugin framework configuration errors.
"""
pass
-class PluginFrameworkStaticException( PluginFrameworkException ):
+
+
+# ============================================================================= base
+class PluginManager( object ):
+ """
+ Plugins represents an section of code that is not tracked in the
+ Galaxy repository, allowing the addition of custom code to a Galaxy
+ installation without changing the code base.
+
+ A PluginManager discovers and manages these plugins.
+
+ This is an non-abstract class but it's usefulness is limited and is meant
+ to be inherited.
+ """
+
+ def __init__( self, app, directories_setting=None, skip_bad_plugins=True, **kwargs ):
+ """
+ Set up the manager and load all plugins.
+
+ :type app: UniverseApplication
+ :param app: the application (and it's configuration) using this manager
+ :type directories_setting: string (default: None)
+ :param directories_setting: the filesystem path (or paths)
+ to search for plugins. Can be CSV string of paths. Will be treated as
+ absolute if a path starts with '/', relative otherwise.
+ :type skip_bad_plugins: boolean (default: True)
+ :param skip_bad_plugins: whether to skip plugins that cause
+ exceptions when loaded or to raise that exception
+ """
+ log.debug( 'PluginManager.init: %s, %s', directories_setting, kwargs )
+ self.directories = []
+ self.skip_bad_plugins = skip_bad_plugins
+ self.plugins = odict.odict()
+
+ self.directories = self.parse_directories_setting( app.config.root, directories_setting )
+ #log.debug( '\t directories: %s', self.directories )
+
+ self.load_configuration()
+ self.load_plugins()
+
+ def parse_directories_setting( self, galaxy_root, directories_setting ):
+ """
+ Parse the ``directories_setting`` into a list of relative or absolute
+ filesystem paths that will be searched to discover plugins.
+
+ :type galaxy_root: string
+ :param galaxy_root: the root path of this galaxy installation
+ :type directories_setting: string (default: None)
+ :param directories_setting: the filesystem path (or paths)
+ to search for plugins. Can be CSV string of paths. Will be treated as
+ absolute if a path starts with '/', relative otherwise.
+ :rtype: list of strings
+ :returns: list of filesystem paths
+ """
+ directories = []
+ if not directories_setting:
+ return directories
+
+ for directory in util.listify( directories_setting ):
+ directory = directory.strip()
+ if directory.startswith( '/' ):
+ directory = os.path.join( galaxy_root, directory )
+ if not os.path.exists( directory ):
+ log.warn( '%s, directory not found: %s', self, directory )
+ continue
+ directories.append( directory )
+ return directories
+
+ def load_configuration( self ):
+ """
+ Override to load some framework/plugin specifc configuration.
+ """
+ # Abstract method
+ return True
+
+ def load_plugins( self ):
+ """
+ Search ``self.directories`` for potential plugins, load them, and cache
+ in ``self.plugins``.
+ :rtype: odict
+ :returns: ``self.plugins``
+ """
+ for plugin_path in self.find_plugins():
+ try:
+ plugin = self.load_plugin( plugin_path )
+ if not plugin:
+ log.warn( '%s, plugin load failed: %s. Skipping...', self, plugin_path )
+ #NOTE: prevent silent, implicit overwrite here (two plugins in two diff directories)
+ #TODO: overwriting may be desired
+ elif plugin.name in self.plugins:
+ log.warn( '%s, plugin with name already exists: %s. Skipping...', self, plugin.name )
+ else:
+ self.plugins[ plugin.name ] = plugin
+ log.info( '%s, loaded plugin: %s', self, plugin.name )
+
+ except Exception, exc:
+ if not self.skip_bad_plugins:
+ raise
+ log.exception( 'Plugin loading raised exception: %s. Skipping...', plugin_path )
+
+ return self.plugins
+
+ def find_plugins( self ):
+ """
+ Return the directory paths of plugins within ``self.directories``.
+
+ Paths are considered a plugin path if they pass ``self.is_plugin``.
+ :rtype: string generator
+ :returns: paths of valid plugins
+ """
+ # due to the ordering of listdir, there is an implicit plugin loading order here
+ # could instead explicitly list on/off in master config file
+ for directory in self.directories:
+ for plugin_dir in os.listdir( directory ):
+ plugin_path = os.path.join( directory, plugin_dir )
+ if self.is_plugin( plugin_path ):
+ yield plugin_path
+
+ def is_plugin( self, plugin_path ):
+ """
+ Determines whether the given filesystem path contains a plugin.
+
+ In this base class, all sub-directories are considered plugins.
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the
+ potential plugin
+ :rtype: bool
+ :returns: True if the path contains a plugin
+ """
+ if not os.path.isdir( plugin_path ):
+ return False
+ return True
+
+ def load_plugin( self, plugin_path ):
+ """
+ Create, load, and/or initialize the plugin and return it.
+
+ Plugin bunches are decorated with:
+ * name : the plugin name
+ * path : the plugin path
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the plugin
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ plugin = bunch.Bunch(
+ #TODO: need a better way to define plugin names
+ # pro: filesystem name ensures uniqueness
+ # con: rel. inflexible
+ name = os.path.split( plugin_path )[1],
+ path = plugin_path
+ )
+ return plugin
+
+
+# ============================================================================= plugin managers using hooks
+class HookPluginManager( PluginManager ):
+ """
+ A hook plugin is a directory containing python modules or packages that:
+ * allow creating, including, and running custom code at specific 'hook'
+ points/events
+ * are not tracked in the Galaxy repository and allow adding custom code
+ to a Galaxy installation
+
+ A HookPluginManager imports the plugin code needed and calls the plugin's
+ hook functions at the specified time.
+ """
+ #: the python file that will be imported - hook functions should be contained here
+ loading_point_filename = 'plugin.py'
+ hook_fn_prefix = 'hook_'
+
+ def is_plugin( self, plugin_path ):
+ """
+ Determines whether the given filesystem path contains a hookable plugin.
+
+ All sub-directories that contain ``loading_point_filename`` are considered
+ plugins.
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the
+ potential plugin
+ :rtype: bool
+ :returns: True if the path contains a plugin
+ """
+ if not super( HookPluginManager, self ).is_plugin( plugin_path ):
+ return False
+ #TODO: possibly switch to .py or __init__.py
+ if self.loading_point_filename not in os.listdir( plugin_path ):
+ return False
+ return True
+
+ def load_plugin( self, plugin_path ):
+ """
+ Import the plugin ``loading_point_filename`` and attach to the plugin bunch.
+
+ Plugin bunches are decorated with:
+ * name : the plugin name
+ * path : the plugin path
+ * module : the plugin code
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the plugin
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ plugin = super( HookPluginManager, self ).load_plugin( plugin_path )
+
+ loading_point_name = self.loading_point_filename[:-3]
+ plugin[ 'module' ] = self.import_plugin_module( loading_point_name, plugin )
+ return plugin
+
+ def import_plugin_module( self, loading_point_name, plugin, import_as=None ):
+ """
+ Import the plugin code and cache the module in the plugin object.
+
+ :type loading_point_name: string
+ :param loading_point_name: name of the python file to import (w/o extension)
+ :type plugin: ``util.bunch.Bunch``
+ :param plugin: the plugin containing the template to render
+ :type import_as: string
+ :param import_as: namespace to use for imported module
+ This will be prepended with the ``__name__`` of this file.
+ Defaults to ``plugin.name``
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ # add this name to import_as (w/ default to plugin.name) to prevent namespace pollution in sys.modules
+ import_as = '%s.%s' %( __name__, ( import_as or plugin.name ) )
+ module_file, pathname, description = imp.find_module( loading_point_name, [ plugin.path ] )
+ try:
+ #TODO: hate this hack but only way to get package imports inside the plugin to work?
+ sys.path.append( plugin.path )
+ # sys.modules will now have import_as in it's list
+ module = imp.load_module( import_as, module_file, pathname, description )
+ finally:
+ module_file.close()
+ if plugin.path in sys.path:
+ sys.path.remove( plugin.path )
+ return module
+
+ def run_hook( self, hook_name, *args, **kwargs ):
+ """
+ Search all plugins for a function named ``hook_fn_prefix`` + ``hook_name``
+ and run it passing in args and kwargs.
+
+ :type hook_name: string
+ :param hook_name: name (suffix) of the hook to run
+ :rtype: 2-tuple containing (list, dict)
+ :returns: (possibly modified) args, kwargs
+ """
+ #TODO: is hook prefix necessary?
+ #TODO: could be made more efficient if cached by hook_name in the manager on load_plugin
+ # (low maint. overhead since no dynamic loading/unloading of plugins)
+ hook_fn_name = ''.join([ self.hook_fn_prefix, hook_name ])
+ for plugin_name, plugin in self.plugins.items():
+ hook_fn = getattr( plugin.module, hook_fn_name, None )
+
+ if hook_fn and hasattr( hook_fn, '__call__' ):
+ try:
+ #log.debug( 'calling %s from %s(%s)', hook_fn.func_name, plugin.name, plugin.module )
+ hook_fn( *args, **kwargs )
+ except Exception, exc:
+ # fail gracefully and continue with other plugins
+ log.exception( 'Hook function "%s" failed for plugin "%s"', hook_name, plugin.name )
+
+ # may have been altered by hook fns, return in order to act like filter
+ return args, kwargs
+
+
+# ============================================================================= exceptions
+class PluginManagerStaticException( PluginManagerException ):
"""Exception for plugin framework static directory set up errors.
"""
pass
-class PluginFrameworkTemplateException( PluginFrameworkException ):
+class PluginManagerTemplateException( PluginManagerException ):
"""Exception for plugin framework template directory
and template rendering errors.
"""
@@ -40,118 +317,139 @@ class PluginFrameworkTemplateException( PluginFrameworkException ):
# ============================================================================= base
-class PluginFramework( object ):
+class PageServingPluginManager( PluginManager ):
"""
- Plugins are files/directories living outside the Galaxy ``lib`` directory
- that serve static files (css, js, images, etc.), use and serve mako templates,
- and have some configuration to control the rendering.
+ Page serving plugins are files/directories that:
+ * are not tracked in the Galaxy repository and allow adding custom code
+ to a Galaxy installation
+ * serve static files (css, js, images, etc.),
+ * render templates
- A plugin framework sets up all the above components.
+ A PageServingPluginManager sets up all the above components.
"""
- #: does the class need a config file(s) to be parsed?
- has_config = True
+ #TODO: I'm unclear of the utility of this class - it prob. will only have one subclass (vis reg). Fold into?
+
#: does the class need static files served?
serves_static = True
#: does the class need template files served?
serves_templates = True
- #TODO: allow plugin mako inheritance from existing ``/templates`` files
- #uses_galaxy_templates = True
- #TODO: possibly better as instance var (or a combo)
- #: the directories in ``plugin_directory`` with basenames listed here will
- #: be ignored for config, static, and templates
- non_plugin_directories = []
+ #: default number of templates to search for plugin template lookup
+ DEFAULT_TEMPLATE_COLLECTION_SIZE = 10
+ #: default encoding of plugin templates
+ DEFAULT_TEMPLATE_ENCODING = 'utf-8'
- # ------------------------------------------------------------------------- setup
- @classmethod
- def from_config( cls, config_plugin_directory, config ):
+ def __init__( self, app, base_url, template_cache_dir=None, **kwargs ):
"""
- Set up the framework based on data from some config object by:
- * constructing it's absolute plugin_directory filepath
- * getting a template_cache
- * and appending itself to the config object's ``plugin_frameworks`` list
+ Set up the manager and load all plugins.
- .. note::
- precondition: config obj should have attributes:
- root, template_cache, and (list) plugin_frameworks
+ :type app: UniverseApplication
+ :param app: the application (and it's configuration) using this manager
+ :type base_url: string
+ :param base_url: url to prefix all plugin urls with
+ :type template_cache_dir: string
+ :param template_cache_dir: filesytem path to the directory where cached
+ templates are kept
"""
- # currently called from (base) app.py - defined here to allow override if needed
- if not config_plugin_directory:
- return None
- try:
- # create the plugin path and if plugin dir begins with '/' assume absolute path
- full_plugin_filepath = os.path.join( config.root, config_plugin_directory )
- if config_plugin_directory.startswith( os.path.sep ):
- full_plugin_filepath = config_plugin_directory
- if not os.path.exists( full_plugin_filepath ):
- raise PluginFrameworkException( 'Plugin path not found: %s' %( full_plugin_filepath ) )
+ self.base_url = base_url
+ self.template_cache_dir = template_cache_dir
- template_cache = config.template_cache if cls.serves_static else None
- plugin = cls( full_plugin_filepath, template_cache )
+ super( PageServingPluginManager, self ).__init__( app, **kwargs )
- config.plugin_frameworks.append( plugin )
- return plugin
-
- except PluginFrameworkException, plugin_exc:
- log.exception( "Error loading framework %s. Skipping...", cls.__class__.__name__ )
- return None
-
- def __str__( self ):
- return '%s(%s)' %( self.__class__.__name__, self.plugin_directories )
-
- def __init__( self, plugin_directories, name=None, template_cache_dir=None, debug=False, assert_exists=True ):
+ def is_plugin( self, plugin_path ):
"""
- :type plugin_directories: string or list
- :param plugin_directories: the base directory where plugin code is kept
- :type name: (optional) string (default: None)
- :param name: the name of this plugin
- (that will appear in url pathing, etc.)
- :type template_cache_dir: (optional) string (default: None)
- :param template_cache_dir: the cache directory to store compiled mako
- :type assert_exists: (optional) bool (default: False)
- :param assert_exists: If True, each configured plugin directory must exist.
+ Determines whether the given filesystem path contains a plugin.
+
+ If the manager ``serves_templates`` and a sub-directory contains another
+ sub-directory named 'templates' it's considered valid.
+ If the manager ``serves_static`` and a sub-directory contains another
+ sub-directory named 'static' it's considered valid.
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the
+ potential plugin
+ :rtype: bool
+ :returns: True if the path contains a plugin
"""
- self.plugin_directories = listify( plugin_directories )
- if assert_exists:
- for plugin_directory in self.plugin_directories:
- if not os.path.isdir( plugin_directory ):
- raise PluginFrameworkException( 'Framework plugin directory not found: %s, %s'
- % ( self.__class__.__name__, plugin_directory ) )
-
- #TODO: or pass in from config
- self.name = name or os.path.basename( self.plugin_directories[0] )
-
- if self.has_config:
- self.load_configuration()
- # set_up_static_urls will be called during the static middleware creation (if serves_static)
- if self.serves_templates:
- self.set_up_templates( template_cache_dir )
-
- def get_plugin_directories( self ):
- """
- Return the plugin directory paths for this plugin.
-
- Gets any directories within ``plugin_directory`` that are directories
- themselves and whose ``basename`` is not in ``plugin_directory``.
- """
- # could instead explicitly list on/off in master config file
- for plugin_directory in self.plugin_directories:
- for plugin_path in glob.glob( os.path.join( plugin_directory, '*' ) ):
- if not os.path.isdir( plugin_path ):
- continue
-
- if os.path.basename( plugin_path ) in self.non_plugin_directories:
- continue
-
- yield plugin_path
-
- # ------------------------------------------------------------------------- config
- def load_configuration( self ):
- """
- Override to load some framework/plugin specifc configuration.
- """
- # Abstract method
+ if not os.path.isdir( plugin_path ):
+ return False
+ #TODO: this is not reliable and forces the inclusion of empty dirs in some situations
+ if self.serves_templates and not 'templates' in os.listdir( plugin_path ):
+ return False
+ if self.serves_static and not 'static' in os.listdir( plugin_path ):
+ return False
return True
+ def load_plugin( self, plugin_path ):
+ """
+ Create the plugin and decorate with static and/or template paths and urls.
+
+ Plugin bunches are decorated with:
+ * name : the plugin name
+ * path : the plugin path
+ * base_url : a url to the plugin
+
+ :type plugin_path: string
+ :param plugin_path: relative or absolute filesystem path to the plugin
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ plugin = super( PageServingPluginManager, self ).load_plugin( plugin_path )
+ #TODO: urlencode?
+ plugin[ 'base_url' ] = '/'.join([ self.base_url, plugin.name ])
+ plugin = self._set_up_static_plugin( plugin )
+ plugin = self._set_up_template_plugin( plugin )
+
+ return plugin
+
+ def _set_up_static_plugin( self, plugin ):
+ """
+ Decorate the plugin with paths and urls needed to serve static content.
+
+ Plugin bunches are decorated with:
+ * serves_static : whether this plugin will serve static content
+
+ If the plugin path contains a 'static' sub-dir, the following are added:
+ * static_path : the filesystem path to the static content
+ * static_url : the url to use when serving static content
+
+ :type plugin: ``util.bunch.Bunch``
+ :param plugin: the plugin to decorate
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ plugin[ 'serves_static' ] = False
+ static_path = os.path.join( plugin.path, 'static' )
+ if self.serves_static and os.path.isdir( static_path ):
+ plugin.serves_static = True
+ plugin[ 'static_path' ] = static_path
+ plugin[ 'static_url' ] = '/'.join([ plugin.base_url, 'static' ])
+ return plugin
+
+ def _set_up_template_plugin( self, plugin ):
+ """
+ Decorate the plugin with paths needed to fill templates.
+
+ Plugin bunches are decorated with:
+ * serves_templates : whether this plugin will use templates
+
+ If the plugin path contains a 'static' sub-dir, the following are added:
+ * template_path : the filesystem path to the template sub-dir
+ * template_lookup : the (currently Mako) TemplateLookup used to search
+ for templates
+
+ :type plugin: ``util.bunch.Bunch``
+ :param plugin: the plugin to decorate
+ :rtype: ``util.bunch.Bunch``
+ :returns: the loaded plugin object
+ """
+ plugin[ 'serves_templates' ] = False
+ template_path = os.path.join( plugin.path, 'templates' )
+ if self.serves_templates and os.path.isdir( template_path ):
+ plugin.serves_templates = True
+ plugin[ 'template_path' ] = template_path
+ plugin[ 'template_lookup' ] = self.build_plugin_template_lookup( plugin )
+ return plugin
+
# ------------------------------------------------------------------------- serving static files
def get_static_urls_and_paths( self ):
"""
@@ -160,81 +458,63 @@ class PluginFramework( object ):
same files.
Meant to be passed to a Static url map.
+
+ :rtype: list of 2-tuples
+ :returns: all urls and paths for each plugin serving static content
"""
- url_and_paths = []
# called during the static middleware creation (buildapp.py, wrap_in_static)
-
- # NOTE: this only searches for static dirs two levels deep (i.e. //static)
- for plugin_path in self.get_plugin_directories():
- # that path is a plugin, search for subdirs named static in THAT dir
- plugin_static_path = os.path.join( plugin_path, 'static' )
- if not os.path.isdir( plugin_static_path ):
- continue
-
- # build a url for that static subdir and create a Static urlmap entry for it
- plugin_name = os.path.splitext( os.path.basename( plugin_path ) )[0]
- plugin_url = self.name + '/' + plugin_name + '/static'
- url_and_paths.append( ( plugin_url, plugin_static_path ) )
-
- return url_and_paths
+ urls_and_paths = []
+ for plugin in self.plugins.values():
+ if plugin.serves_static:
+ urls_and_paths.append( ( plugin.static_url, plugin.static_path ) )
+ return urls_and_paths
# ------------------------------------------------------------------------- templates
- def set_up_templates( self, template_cache_dir ):
+ def build_plugin_template_lookup( self, plugin ):
"""
- Add a ``template_lookup`` attribute to the framework that can be passed
- to the mako renderer to find templates.
- """
- if not template_cache_dir:
- raise PluginFrameworkTemplateException( 'Plugins that serve templates require a template_cache_dir' )
- self.template_lookup = self._create_mako_template_lookup( template_cache_dir, self._get_template_paths() )
- return self.template_lookup
+ Builds the object that searches for templates (cached or not) when rendering.
- def _get_template_paths( self ):
+ :type plugin: ``util.bunch.Bunch``
+ :param plugin: the plugin containing the templates
+ :rtype: ``Mako.lookup.TemplateLookup``
+ :returns: template lookup for this plugin
"""
- Get the paths that will be searched for templates.
- """
- return self.plugin_directories
+ if not plugin.serves_templates:
+ return None
+ template_lookup = self._create_mako_template_lookup( self.template_cache_dir, plugin.template_path )
+ return template_lookup
- def _create_mako_template_lookup( self, cache_dir, paths, collection_size=500, output_encoding='utf-8' ):
+ def _create_mako_template_lookup( self, cache_dir, paths,
+ collection_size=DEFAULT_TEMPLATE_COLLECTION_SIZE, output_encoding=DEFAULT_TEMPLATE_ENCODING ):
"""
Create a ``TemplateLookup`` with defaults.
+
+ :rtype: ``Mako.lookup.TemplateLookup``
+ :returns: all urls and paths for each plugin serving static content
"""
+ #TODO: possible to add galaxy/templates into the lookup here?
return mako.lookup.TemplateLookup(
directories = paths,
module_directory = cache_dir,
collection_size = collection_size,
output_encoding = output_encoding )
- #TODO: do we want to remove trans and app from the plugin template context?
- def fill_template( self, trans, template_filename, **kwargs ):
+ def fill_template( self, trans, plugin, template_filename, **kwargs ):
"""
- Pass control over to trans and render the ``template_filename``.
+ Pass control over to trans and render ``template_filename``.
+
+ :type trans: ``galaxy.web.framework.GalaxyWebTransaction``
+ :param trans: transaction doing the rendering
+ :type plugin: ``util.bunch.Bunch``
+ :param plugin: the plugin containing the template to render
+ :type template_filename: string
+ :param template_filename: the path of the template to render relative to
+ ``plugin.template_path``
+ :returns: rendered template
"""
# defined here to be overridden
- return trans.fill_template( template_filename, template_lookup=self.template_lookup, **kwargs )
+ return trans.fill_template( template_filename, template_lookup=plugin.template_lookup, **kwargs )
- def fill_template_with_plugin_imports( self, trans, template_filename, **kwargs ):
- """
- Returns a rendered plugin template but allows importing modules from inside
- the plugin directory within the template.
-
- ..example:: I.e. given this layout for a plugin:
- bler/
- template/
- bler.mako
- static/
- conifg/
- my_script.py
- this version of `fill_template` allows `bler.mako` to call `import my_script`.
- """
- try:
- plugin_path = os.path.dirname( os.path.dirname( template_filename ) )
- sys.path.append( plugin_path )
- filled_template = self.fill_template( trans, template_filename, **kwargs )
-
- finally:
- sys.path.remove( plugin_path )
-
- return filled_template
-
- #TODO: could add plugin template helpers here
+ #TODO: add fill_template fn that is able to load extra libraries beforehand (and remove after)
+ #TODO: add template helpers specific to the plugins
+ #TODO: some sort of url_for for these plugins
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index b2f727511a6..7cdab12a50d 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -193,7 +193,8 @@ def app_factory( global_conf, **kwargs ):
if kwargs.get( 'middleware', True ):
webapp = wrap_in_middleware( webapp, global_conf, **kwargs )
if asbool( kwargs.get( 'static_enabled', True ) ):
- webapp = wrap_in_static( webapp, global_conf, plugin_frameworks=app.config.plugin_frameworks, **kwargs )
+ webapp = wrap_in_static( webapp, global_conf, plugin_frameworks=[ app.visualizations_registry ], **kwargs )
+ #webapp = wrap_in_static( webapp, global_conf, plugin_frameworks=None, **kwargs )
if asbool(kwargs.get('pack_scripts', False)):
pack_scripts()
# Close any pooled database connections before forking
@@ -362,12 +363,13 @@ def wrap_in_static( app, global_conf, plugin_frameworks=None, **local_conf ):
# wrap any static dirs for plugins
plugin_frameworks = plugin_frameworks or []
- for static_serving_framework in ( framework for framework in plugin_frameworks if framework.serves_static ):
- # invert control to each plugin for finding their own static dirs
- for plugin_url, plugin_static_path in static_serving_framework.get_static_urls_and_paths():
- plugin_url = '/plugins/' + plugin_url
- urlmap[( plugin_url )] = Static( plugin_static_path, cache_time )
- log.debug( 'added url, path to static middleware: %s, %s', plugin_url, plugin_static_path )
+ for framework in plugin_frameworks:
+ if framework and framework.serves_static:
+ # invert control to each plugin for finding their own static dirs
+ for plugin_url, plugin_static_path in framework.get_static_urls_and_paths():
+ plugin_url = '/plugins/' + plugin_url
+ urlmap[( plugin_url )] = Static( plugin_static_path, cache_time )
+ log.debug( 'added url, path to static middleware: %s, %s', plugin_url, plugin_static_path )
# URL mapper becomes the root webapp
return urlmap
diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py
index 7ab97073166..e422709cffb 100644
--- a/lib/galaxy/webapps/galaxy/controllers/visualization.py
+++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py
@@ -698,10 +698,10 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
registry = trans.app.visualizations_registry
if not registry:
raise HTTPNotFound( 'No visualization registry (possibly disabled in universe_wsgi.ini)' )
- if visualization_name not in registry.listings:
+ if visualization_name not in registry.plugins:
raise HTTPNotFound( 'Unknown or invalid visualization: ' + visualization_name )
# or redirect to list?
- registry_listing = registry.listings[ visualization_name ]
+ plugin = registry.plugins[ visualization_name ]
returned = None
try:
@@ -711,8 +711,8 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
resources = registry.query_dict_to_resources( trans, self, visualization_name, kwargs )
# look up template and render
- template_path = registry_listing[ 'template' ]
- returned = registry.fill_template( trans, template_path,
+ template_path = plugin.config[ 'template' ]
+ returned = registry.fill_template( trans, plugin, template_path,
visualization_name=visualization_name, query_args=kwargs,
embedded=embedded, shared_vars={}, **resources )
#NOTE: passing *unparsed* kwargs as query_args