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.
Contents
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.
Page.py fallback to plain text parser when wiki parser is not available.
Trying to load a theme and falling back to built in 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:
With:
You want to import a name from a plugin
Replace:
With:
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:
- Keep plugin API simple - no magic or smart code to hide the fact that a dependency is not there, and:
Make rst parser an additional plugin, in the contrib directory.
- Or add docutils to moinmoin, like we do with lupy
Or Let the plugin handle this problem and show a message, for example, write "doutils is missing" and then format the text using plain.py
- But then each plugin will have to repeat the same error handling code, while we already have one point that handle "no plugin" errors, in the parser or the formatter.
Or Let the caller handle the ImportError
This is bad because the error might be a stupid typo like import doutil instead of import docutils, and your error is hidden in an evil way. Or the error is caused not in the plugin module, but in the code it try to import, then you don't know if your code is broken or the dependency is broken.
Change the plugin API to recognize plugin dependency error. For example, create a sub class of PluginMissingError, PluginDependencyError, which is a special error where plugin is missing because a dependency is not installed. In this case, rst will use this code:
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:
When trying to import a name from a plugin, check for PluginDependencyError
- 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'
Future development
It would be easier if importPlugin would return a plugin module, and not a name in the module:
This is like static import: