Developing MoinMoin plugins used to be a nightmare because errors where often hidden by the plugin import code. The typical case was a wiki plugin that "does not work" with no error message. In patch-934 error handling was finally fixed. This page describes how it works and shows example code using the new API.

See also MoinDev/PluginConcept.

How plugin errors are handled

Error

action

1. no such plugin

continue without the plugin

2. can't load plugin, e.g syntax error

fail with traceback

3. plugin does not have a required attribute

fail with traceback

4. plugin does not have an optional attribute

continue without the option

Old (pre patch-930) plugin code will return None in cases 1, 3 and 4. Obviously, you can't continue or fail with traceback when you get None in both cases :)

The plugin system should return the correct error in each case:

Error

result

1. no such plugin

raise PluginMissingError

2. can't load plugin, e.g syntax error

raise the error

3. plugin does not have a required attribute

raise PluginAttribtueError

4. plugin does not have an optional attribute

raise PluginAttribtueError, the calling code should catch it and do the right thing

5. Any other error in the plugin or code imported by the plugin

pass the error to the calling code, usually will fail with a detailed traceback

Required plugin names

Plugins must provide certain names:

Plugin

Required

macro

execute

theme

execute, Theme

parser

Parser

processor

process

action

execute

formatter

Formatter

xmlrpc

?

New code examples

Here are some examples of code importing plugins. For more examples, grep for importPlugin.

Get a required plugin

The plugin is either the wiki plugin or the builtin plugin. Any error in the selected plugin can not be fixed by this code, and will fail with a traceback.

   1     parser = wikiutil.importPlugin(request.cfg, 'parser', 'wiki')

If you installed your own wiki parser, and its broken, the only way to fix the traceback is either fix the broken code or remove the wiki plugin.

Trying to load a theme and falling back to built in theme

   1 try:
   2    themeClass = wikiutil.importPlugin(request.cfg, 'theme', 'modern', 'Theme')
   3 except wikiutil.PluginMissingError:
   4    from MoinMoin.theme import modern
   5    themeClass = modern.Theme

Trying to get optional attribute

   1 try:
   2     myMacro = wikiutil.importPlugin(request.cfg, 'macro', 'MyMacro', 'execute')
   3 except wikiutil.PluginMissingError:
   4    # continue without the macro
   5 else:
   6     try:
   7         dependencies = wikiutil.importPlugin(request.cfg, 'macro', 'MyMacro', 'Dependencies')
   8     except wikiutil.PluginAttributeError:
   9         dependencies = defaultDependencies
  10     
  11     myMacro(macro, args)

Fixing your code

3rd party code that expect to get None when a plugin is not available will break, and must check for wikiutil.PluginMissingError or wikiutil.PluginAttributeError, or simply wikiutil.PluginError, if you want to catch both errors. This effect 3rd party plugins that import other plugins.

You want to import another plugin

Replace:

   1 foo = wikiutil.importPlugin(cfg, 'macro', 'Foo')
   2 if foo is None:
   3     # continue without foo

With:

   1 try:
   2     foo = wikiutil.importPlugin(cfg, 'macro', 'Foo')
   3 except wikiutil.PluginMissingError:
   4     # continue without foo

You want to import a name from a plugin

Replace:

   1 caching = wikiutil.importPlugin(request.cfg, 'parser', 'wiki', 'caching')
   2 if caching is None:
   3     # continue without caching

With:

   1 try:
   2     caching = wikiutil.importPlugin(request.cfg, 'parser', 'wiki', 'caching')
   3 except wikiutil.PluginMissingError:
   4     # continue without caching

You want your plugin to run on 1.3.0 and later

Add this function to your code, and use it to import plugins, instead of the builtin importPlugin.

   1 def importPlugin(cfg, kind, name, function="execute"):
   2     """ Import plugin supporting both new and old error handling
   3 
   4     To port old plugins to new moin releases, copy this into your plugin
   5     and use it instead of wikiutil.importPlugin. Your code would run on
   6     both 1.3 and later.
   7     """
   8     if hasattr(wikiutil, 'PluginMissingError'):
   9         # New error handling: missing plugins ignored, other errors in
  10         # plugin code will be raised.
  11         try:
  12             Plugin = wikiutil.importPlugin(cfg, kind, name, function)
  13         except wikiutil.PluginMissingError:
  14             Plugin = None
  15     else:
  16         # Old error handling: most errors in plugin code will be hidden.
  17         Plugin = wikiutil.importPlugin(cfg, kind, name, function)
  18 
  19     return Plugin

Builtin plugins which depend on external code

There is a special case with builtin plugins that depends on external software, for example, parser/rst.py. If docutils is not installed, the import fail, and you get an exception.

Getting a correct exception is the goal of the new plugin error handling, and is a good thing. But rst is builtin plugin, and we can't ship a plguin that break when external software is not available. It should work like GDChart - if its not there, we use alternative format.

The options are:

   1 try:
   2     import docutils
   3 except ImportError, err:
   4     raise wikiutil.PluginDependencyError(str(err))

The code trying to import rst already check for PluginMissingError, and will treat the the rst plugin as missing plugin.

The use will get plain text when he try to use rst:

\#!rst (-)

Plain text

Patch

Here is a patch using this option. This patch does two things:

  1. When trying to import a name from a plugin, check for PluginDependencyError

  2. When raised, unregister the plugin, so we don't try to import this plugin again.

   1 * looking for nirs@freeshell.org--2005/moin--fix--1.3--patch-65 to compare with
   2 * comparing to nirs@freeshell.org--2005/moin--fix--1.3--patch-65
   3 M  MoinMoin/parser/rst.py
   4 M  MoinMoin/wikiutil.py
   5 
   6 * modified files
   7 
   8 --- orig/MoinMoin/parser/rst.py
   9 +++ mod/MoinMoin/parser/rst.py
  10 @@ -40,7 +40,12 @@
  11      urlopen = staticmethod(urlopen)
  12  
  13  # # # All docutils imports must be contained below here
  14 -import docutils
  15 +try:
  16 +    import docutils
  17 +except ImportError, err:
  18 +    from MoinMoin import wikiutil
  19 +    raise wikiutil.PluginDependencyError(str(err))
  20 +    
  21  from docutils.core import publish_parts
  22  from docutils.writers import html4css1
  23  from docutils.nodes import fully_normalize_name, reference
  24 
  25 
  26 --- orig/MoinMoin/wikiutil.py
  27 +++ mod/MoinMoin/wikiutil.py
  28 @@ -544,6 +544,9 @@
  29  class PluginMissingError(PluginError):
  30      """ Raised when a plugin is not found """
  31  
  32 +class PluginDependencyError(PluginMissingError):
  33 +    """ Raised when plugin can't get a dependency """
  34 +
  35  class PluginAttributeError(PluginError):
  36      """ Raised when plugin does not contain an attribtue """
  37  
  38 @@ -583,8 +586,8 @@
  39      """
  40      if not name in wikiPlugins(kind, cfg):
  41          raise PluginMissingError
  42 -    moduleName = '%s.plugin.%s.%s' % (cfg.siteid, kind, name)
  43 -    return importNameFromPlugin(moduleName, function)
  44 +    package = '%s.plugin.%s' % (cfg.siteid, kind)
  45 +    return importNameFromPlugin(package, name, function)
  46  
  47  
  48  def importBuiltinPlugin(kind, name, function):
  49 @@ -594,21 +597,38 @@
  50      """
  51      if not name in builtinPlugins(kind):
  52          raise PluginMissingError
  53 -    moduleName = 'MoinMoin.%s.%s' % (kind, name)
  54 -    return importNameFromPlugin(moduleName, function)
  55 +    package = 'MoinMoin.%s' % kind
  56 +    return importNameFromPlugin(package, name, function)
  57  
  58  
  59 -def importNameFromPlugin(moduleName, name):
  60 +def importNameFromPlugin(package, plugin, name):
  61      """ Return name from plugin module 
  62      
  63 -    Raise PluginAttributeError if name does not exists.
  64 +    Raise PluginDependencyError if the plugin raise it. Raise
  65 +    PluginAttributeError if name does not exists.
  66      """
  67 -    module = __import__(moduleName, globals(), {}, [name])
  68 +    try:
  69 +        qualifiedName = '%s.%s' % (package, plugin)
  70 +        module = __import__(qualifiedName, globals, {}, [name])
  71 +    except PluginDependencyError:
  72 +        unregisterPlugin(package, plugin)
  73 +        raise
  74 +    try:
  75 +        return getattr(module, name)
  76 +    except AttributeError:
  77 +        raise PluginAttributeError
  78 +        
  79      try:
  80          return getattr(module, name)
  81      except AttributeError:
  82          raise PluginAttributeError
  83  
  84 +
  85 +def unregisterPlugin(package, plugin):
  86 +    """ Remove a plugin from the plugin registry """
  87 +    package = __import__(package, globals, {}, ['modules'])
  88 +    package.modules.remove(plugin)
  89 +    
  90  
  91  def builtinPlugins(kind):
  92      """ Gets a list of modules in MoinMoin.'kind'
dependency.patch

Future development

It would be easier if importPlugin would return a plugin module, and not a name in the module:

   1 try:
   2     myMacro = wikiutil.importPlugin(request.cfg, 'macro', 'MyMacro')
   3 except wikiutil.PluginMissingError:
   4    # continue without the macro
   5 else:
   6     dependencies = getattr(myMacro, 'Dependencies', defaultDependencies)
   7     myMacro.execute(macro, args)

This is like static import:

   1 from MoinMoin.macro import MyMacro
   2 dependencies = getattr(MyMacro, 'Dependencies', defaultDependencies)
   3 MyMacro.execute(macro, args)

MoinMoin: ErrorHandlingInPlugins (last edited 2007-10-29 19:13:39 by localhost)