Plugin framework: factor out page serving responsibility, add HookPluginManager subclass and PageServingPluginManager subclass; Visualization registry: inherit from PageServingPluginManager

This commit is contained in:
Carl Eberhard
2013-09-18 10:53:12 -04:00
parent 0ab2b1b023
commit 3a2cb607ac
8 changed files with 543 additions and 270 deletions
@@ -16,5 +16,5 @@
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<template>graphview/templates/graphview.mako</template>
<template>graphview.mako</template>
</visualization>
@@ -11,5 +11,5 @@
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<template>scatterplot/templates/scatterplot.mako</template>
<template>scatterplot.mako</template>
</visualization>
+7 -2
View File
@@ -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 )
+1 -3
View File
@@ -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
+82 -94
View File
@@ -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 ``<plugin_path>/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 ):
"""
+438 -158
View File
@@ -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 <plugin.name>.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. <plugin_directory>/<plugin-name>/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
+9 -7
View File
@@ -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
@@ -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