commit/galaxy-central: carlfeberhard: Plugin framework: factor out page serving responsibility, add HookPluginManager subclass and PageServingPluginManager subclass; Visualization registry: inherit from PageServingPluginManager
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/2d0bcb07596c/ Changeset: 2d0bcb07596c User: carlfeberhard Date: 2013-09-18 16:53:12 Summary: Plugin framework: factor out page serving responsibility, add HookPluginManager subclass and PageServingPluginManager subclass; Visualization registry: inherit from PageServingPluginManager Affected #: 8 files diff -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 config/plugins/visualizations/graphview/config/graphview.xml --- a/config/plugins/visualizations/graphview/config/graphview.xml +++ b/config/plugins/visualizations/graphview/config/graphview.xml @@ -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> diff -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 config/plugins/visualizations/scatterplot/config/scatterplot.xml --- a/config/plugins/visualizations/scatterplot/config/scatterplot.xml +++ b/config/plugins/visualizations/scatterplot/config/scatterplot.xml @@ -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> diff -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/app.py --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -22,6 +22,8 @@ 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 @@ # 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 -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -294,9 +294,7 @@ 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 -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/visualization/registry.py --- a/lib/galaxy/visualization/registry.py +++ b/lib/galaxy/visualization/registry.py @@ -39,9 +39,10 @@ """ # ------------------------------------------------------------------- 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 @@ '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 @@ `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """ 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 @@ 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 ): """ diff -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/web/base/pluginframework.py --- a/lib/galaxy/web/base/pluginframework.py +++ b/lib/galaxy/web/base/pluginframework.py @@ -1,150 +1,106 @@ """ 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 ): - """Exception for plugin framework static directory set up errors. - """ - pass -class PluginFrameworkTemplateException( PluginFrameworkException ): - """Exception for plugin framework template directory - and template rendering errors. - """ - pass # ============================================================================= base -class PluginFramework( object ): +class PluginManager( object ): """ - 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. + 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 plugin framework sets up all the above components. + 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. """ - #: does the class need a config file(s) to be parsed? - has_config = True - #: 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 = [] - # ------------------------------------------------------------------------- setup - @classmethod - def from_config( cls, config_plugin_directory, config ): + def __init__( self, app, directories_setting=None, skip_bad_plugins=True, **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 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 """ - # 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 ) ) + log.debug( 'PluginManager.init: %s, %s', directories_setting, kwargs ) + self.directories = [] + self.skip_bad_plugins = skip_bad_plugins + self.plugins = odict.odict() - template_cache = config.template_cache if cls.serves_static else None - plugin = cls( full_plugin_filepath, template_cache ) + self.directories = self.parse_directories_setting( app.config.root, directories_setting ) + #log.debug( '\t directories: %s', self.directories ) - config.plugin_frameworks.append( plugin ) - return plugin + self.load_configuration() + self.load_plugins() - except PluginFrameworkException, plugin_exc: - log.exception( "Error loading framework %s. Skipping...", cls.__class__.__name__ ) - return None + 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. - def __str__( self ): - return '%s(%s)' %( self.__class__.__name__, self.plugin_directories ) + :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 - def __init__( self, plugin_directories, name=None, template_cache_dir=None, debug=False, assert_exists=True ): - """ - :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. - """ - 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 ) ) + 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 - #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. @@ -152,6 +108,348 @@ # 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 PluginManagerTemplateException( PluginManagerException ): + """Exception for plugin framework template directory + and template rendering errors. + """ + pass + + +# ============================================================================= base +class PageServingPluginManager( PluginManager ): + """ + 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 PageServingPluginManager sets up all the above components. + """ + #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 + #: 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' + + def __init__( self, app, base_url, template_cache_dir=None, **kwargs ): + """ + Set up the manager and load all plugins. + + :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 + """ + self.base_url = base_url + self.template_cache_dir = template_cache_dir + + super( PageServingPluginManager, self ).__init__( app, **kwargs ) + + def is_plugin( self, plugin_path ): + """ + 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 + """ + 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 @@ 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. + Builds the object that searches for templates (cached or not) when rendering. + + :type plugin: ``util.bunch.Bunch`` + :param plugin: the plugin containing the templates + :rtype: ``Mako.lookup.TemplateLookup`` + :returns: template lookup for this plugin """ - 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 + 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 _get_template_paths( self ): - """ - Get the paths that will be searched for templates. - """ - return self.plugin_directories - - 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 -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -193,7 +193,8 @@ 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 @@ # 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 -r b5c08cc6adbb64bb8dfa20070e6df5857483bd00 -r 2d0bcb07596c4c688ed3908391d866831c65c042 lib/galaxy/webapps/galaxy/controllers/visualization.py --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -698,10 +698,10 @@ 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 @@ 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 Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org