changeset 1483:0b790f47253a

merge with xapian branch
author Franz Pletz <fpletz AT franz-pletz DOT org>
date Thu, 24 Aug 2006 19:37:25 +0200
parents 546ec510caa1 (current diff) 68ed96d511f5 (diff)
children 499517b99c5f
files
diffstat 6 files changed, 384 insertions(+), 217 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/SyncPages.py	Thu Aug 24 17:14:26 2006 +0200
+++ b/MoinMoin/action/SyncPages.py	Thu Aug 24 19:37:25 2006 +0200
@@ -11,6 +11,9 @@
 import os
 import re
 import xmlrpclib
+import traceback
+import StringIO # not relevant for speed, so we do not need cStringIO
+
 
 # Compatiblity to Python 2.3
 try:
@@ -27,7 +30,7 @@
 from MoinMoin.wikisync import TagStore, UnsupportedWikiException, SyncPage, NotAllowedException
 from MoinMoin.wikisync import MoinLocalWiki, MoinRemoteWiki, UP, DOWN, BOTH, MIMETYPE_MOIN
 from MoinMoin.util.bdiff import decompress, patch, compress, textdiff
-from MoinMoin.util import diff3
+from MoinMoin.util import diff3, rpc_aggregator
 
 
 debug = False
@@ -48,11 +51,30 @@
         self.pagename = pagename
         self.page = PageEditor(request, pagename)
         self.status = []
+        self.rollback = set()
 
     def log_status(self, level, message="", substitutions=(), raw_suffix=""):
         """ Appends the message with a given importance level to the internal log. """
         self.status.append((level, message, substitutions, raw_suffix))
 
+    def register_rollback(self, func):
+        self.rollback.add(func)
+
+    def remove_rollback(self, func):
+        self.rollback.remove(func)
+
+    def call_rollback_funcs(self):
+        _ = lambda x: x
+
+        for func in self.rollback:
+            try:
+                page_name = func()
+                self.log_status(self.INFO, _("Rolled back changes to the page %s."), (page_name, ))
+            except Exception, e:
+                temp_file = StringIO.StringIO()
+                traceback.print_exc(file=temp_file)
+                self.log_status(self.ERROR, _("Exception while calling rollback function:"), raw_suffix=temp_file.getvalue())
+
     def generate_log_table(self):
         """ Transforms self.status into a user readable table. """
         table_line = u"|| %(smiley)s || %(message)s%(raw_suffix)s ||"
@@ -69,7 +91,7 @@
                 message = u""
             table.append(table_line % {"smiley": line[0][1],
                                        "message": message,
-                                       "raw_suffix": line[3]})
+                                       "raw_suffix": line[3].replace("\n", "[[BR]]")})
 
         return "\n".join(table)
 
@@ -146,19 +168,22 @@
                 raise ActionStatus(_("The ''remoteWiki'' is unknown."))
         except ActionStatus, e:
             msg = u'<p class="error">%s</p>\n' % (e.args[0], )
-
-        try:
+        else:
             try:
-                self.sync(params, local, remote)
-            except Exception, e:
-                self.log_status(self.ERROR, _("A severe error occured:"), raw_suffix=repr(e))
-                raise
-            else:
-                msg = u"%s" % (_("Syncronisation finished. Look below for the status messages."), )
-        finally:
-            # XXX aquire readlock on self.page
-            self.page.saveText(self.page.get_raw_body() + "\n\n" + self.generate_log_table(), 0)
-            # XXX release readlock on self.page
+                try:
+                    self.sync(params, local, remote)
+                except Exception, e:
+                    temp_file = StringIO.StringIO()
+                    traceback.print_exc(file=temp_file)
+                    self.log_status(self.ERROR, _("A severe error occured:"), raw_suffix=temp_file.getvalue())
+                    raise
+                else:
+                    msg = u"%s" % (_("Syncronisation finished. Look below for the status messages."), )
+            finally:
+                # XXX aquire readlock on self.page
+                self.call_rollback_funcs()
+                self.page.saveText(self.page.get_raw_body() + "\n\n" + self.generate_log_table(), 0)
+                # XXX release readlock on self.page
 
         self.page.send_page(self.request, msg=msg)
 
@@ -170,7 +195,7 @@
                 Wiki A    | Wiki B   | Remark
                 ----------+----------+------------------------------
                 exists    | non-     | Now the wiki knows that the page was renamed.
-                with tags | existant | There should be an RPC method that asks
+                with tags | existing | There should be an RPC method that asks
                           |          | for the new name (which could be recorded
                           |          | on page rename). Then the page is
                           |          | renamed in Wiki A as well and the sync
@@ -219,179 +244,192 @@
             m_pages = SyncPage.filter(m_pages, params["pageMatch"].match)
             self.log_status(self.INFO, _("After filtering: %s pages"), (str(len(m_pages)), ))
 
-        def handle_page(sp):
-            # XXX add locking, acquire read-lock on sp
-            if debug:
-                self.log_status(ActionClass.INFO, raw_suffix="Processing %r" % sp)
-
-            local_pagename = sp.local_name
-            current_page = PageEditor(self.request, local_pagename) # YYY direct access
-            comment = u"Local Merge - %r" % (remote.get_interwiki_name() or remote.get_iwid())
-
-            tags = TagStore(current_page)
-
-            matching_tags = tags.fetch(iwid_full=remote.iwid_full, direction=match_direction)
-            matching_tags.sort()
-            if debug:
-                self.log_status(ActionClass.INFO, raw_suffix="Tags: %r [[BR]] All: %r" % (matching_tags, tags.tags))
-
-            # some default values for non matching tags
-            normalised_name = None
-            remote_rev = None
-            local_rev = sp.local_rev # merge against the newest version
-            old_contents = ""
-
-            if matching_tags:
-                newest_tag = matching_tags[-1]
-
-                local_change = newest_tag.current_rev != sp.local_rev
-                remote_change = newest_tag.remote_rev != sp.remote_rev
-
-                # handle some cases where we cannot continue for this page
-                if not remote_change and (direction == DOWN or not local_change):
-                    return # no changes done, next page
-                if sp.local_deleted and sp.remote_deleted:
-                    return
-                if sp.remote_deleted and not local_change:
-                    msg = local.delete_page(sp.local_name, comment)
-                    if not msg:
-                        self.log_status(ActionClass.INFO, _("Deleted page %s locally."), (sp.name, ))
-                    else:
-                        self.log_status(ActionClass.ERROR, _("Error while deleting page %s locally:"), (sp.name, ), msg)
-                    return
-                if sp.local_deleted and not remote_change:
-                    if direction == DOWN:
-                        return
-                    msg = remote.delete_page(sp.remote_name, sp.remote_rev, local_full_iwid)
-                    if not msg:
-                        self.log_status(ActionClass.INFO, _("Deleted page %s remotely."), (sp.name, ))
-                    else:
-                        self.log_status(ActionClass.ERROR, _("Error while deleting page %s remotely:"), (sp.name, ), msg)
-                    return
-                if sp.local_mime_type != MIMETYPE_MOIN and not (local_change ^ remote_change):
-                    self.log_status(ActionClass.WARN, _("The item %s cannot be merged but was changed in both wikis. Please delete it in one of both wikis and try again."), (sp.name, ))
-                    return
-                if sp.local_mime_type != sp.remote_mime_type:
-                    self.log_status(ActionClass.WARN, _("The item %s has different mime types in both wikis and cannot be merged. Please delete it in one of both wikis or unify the mime type, and try again."), (sp.name, ))
-                    return
-                if newest_tag.normalised_name != sp.name:
-                    self.log_status(ActionClass.WARN, _("The item %s was renamed locally. This is not implemented yet. Therefore the full syncronisation history is lost for this page."), (sp.name, )) # XXX implement renames
-                else:
-                    normalised_name = newest_tag.normalised_name
-                    local_rev = newest_tag.current_rev
-                    remote_rev = newest_tag.remote_rev
-                    old_contents = Page(self.request, local_pagename, rev=newest_tag.current_rev).get_raw_body_str() # YYY direct access
-
-            self.log_status(ActionClass.INFO, _("Synchronising page %s with remote page %s ..."), (local_pagename, sp.remote_name))
-
-            if direction == DOWN:
-                remote_rev = None # always fetch the full page, ignore remote conflict check
-                patch_base_contents = ""
-            else:
-                patch_base_contents = old_contents
+        class handle_page(rpc_aggregator.RPCYielder):
+            def run(yielder, sp):
+                # XXX add locking, acquire read-lock on sp
+                if debug:
+                    self.log_status(ActionClass.INFO, raw_suffix="Processing %r" % sp)
+    
+                local_pagename = sp.local_name
+                current_page = PageEditor(self.request, local_pagename) # YYY direct access
+                comment = u"Local Merge - %r" % (remote.get_interwiki_name() or remote.get_iwid())
 
-            if remote_rev != sp.remote_rev:
-                if sp.remote_deleted: # ignore remote changes
-                    current_remote_rev = sp.remote_rev
-                    is_remote_conflict = False
-                    diff = None
-                    self.log_status(ActionClass.WARN, _("The page %s was deleted remotely but changed locally."), (sp.name, ))
-                else:
-                    diff_result = remote.get_diff(sp.remote_name, remote_rev, None, normalised_name)
-                    if diff_result is None:
-                        self.log_status(ActionClass.ERROR, _("The page %s could not be synced. The remote page was renamed. This is not supported yet. You may want to delete one of the pages to get it synced."), (sp.remote_name, ))
-                        return
-                    is_remote_conflict = diff_result["conflict"]
-                    assert diff_result["diffversion"] == 1
-                    diff = diff_result["diff"]
-                    current_remote_rev = diff_result["current"]
-            else:
-                current_remote_rev = remote_rev
-                if sp.local_mime_type == MIMETYPE_MOIN:
-                    is_remote_conflict = wikiutil.containsConflictMarker(old_contents.decode("utf-8"))
-                else:
-                    is_remote_conflict = NotImplemented
-                diff = None
-
-            # do not sync if the conflict is remote and local, or if it is local
-            # and the page has never been syncronised
-            if (sp.local_mime_type == MIMETYPE_MOIN and wikiutil.containsConflictMarker(current_page.get_raw_body()) # YYY direct access
-                and (remote_rev is None or is_remote_conflict)):
-                self.log_status(ActionClass.WARN, _("Skipped page %s because of a locally or remotely unresolved conflict."), (local_pagename, ))
-                return
-
-            if remote_rev is None and direction == BOTH:
-                self.log_status(ActionClass.INFO, _("This is the first synchronisation between the local and the remote wiki for the page %s."), (sp.name, ))
-
-            if sp.remote_deleted:
-                remote_contents = ""
-            elif diff is None:
-                remote_contents = old_contents
-            else:
-                remote_contents = patch(patch_base_contents, decompress(diff))
+                tags = TagStore(current_page)
 
-            if sp.local_mime_type == MIMETYPE_MOIN:
-                remote_contents_unicode = remote_contents.decode("utf-8")
-                # here, the actual 3-way merge happens
-                merged_text = diff3.text_merge(old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), 1, *conflict_markers) # YYY direct access
+                matching_tags = tags.fetch(iwid_full=remote.iwid_full, direction=match_direction)
+                matching_tags.sort()
                 if debug:
-                    self.log_status(ActionClass.INFO, raw_suffix="Merging %r, %r and %r into %r" % (old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), merged_text))
-                merged_text_raw = merged_text.encode("utf-8")
-            else:
-                if diff is None:
-                    merged_text_raw = remote_contents
-                else:
-                    merged_text_raw = current_page.get_raw_body_str() # YYY direct access
-
-            diff = textdiff(remote_contents, merged_text_raw)
-            if debug:
-                self.log_status(ActionClass.INFO, raw_suffix="Diff against %r" % remote_contents)
+                    self.log_status(ActionClass.INFO, raw_suffix="Tags: %r [[BR]] All: %r" % (matching_tags, tags.tags))
 
-            # XXX upgrade to write lock
-            try:
-                local_change_done = True
-                current_page.saveText(merged_text, sp.local_rev or 0, comment=comment) # YYY direct access
-            except PageEditor.Unchanged:
-                local_change_done = False
-            except PageEditor.EditConflict:
-                local_change_done = False
-                assert False, "You stumbled on a problem with the current storage system - I cannot lock pages"
+                # some default values for non matching tags
+                normalised_name = None
+                remote_rev = None
+                local_rev = sp.local_rev # merge against the newest version
+                old_contents = ""
 
-            new_local_rev = current_page.get_real_rev() # YYY direct access
+                if matching_tags:
+                    newest_tag = matching_tags[-1]
+    
+                    local_change = newest_tag.current_rev != sp.local_rev
+                    remote_change = newest_tag.remote_rev != sp.remote_rev
 
-            def rollback_local_change(): # YYY direct local access
-                rev = new_local_rev - 1
-                revstr = '%08d' % rev
-                oldpg = Page(self.request, sp.local_name, rev=rev)
-                pg = PageEditor(self.request, sp.local_name)
-                savemsg = pg.saveText(oldpg.get_raw_body(), 0, comment=u"Wikisync rollback", extra=revstr, action="SAVE/REVERT")
+                    # handle some cases where we cannot continue for this page
+                    if not remote_change and (direction == DOWN or not local_change):
+                        return # no changes done, next page
+                    if sp.local_deleted and sp.remote_deleted:
+                        return
+                    if sp.remote_deleted and not local_change:
+                        msg = local.delete_page(sp.local_name, comment)
+                        if not msg:
+                            self.log_status(ActionClass.INFO, _("Deleted page %s locally."), (sp.name, ))
+                        else:
+                            self.log_status(ActionClass.ERROR, _("Error while deleting page %s locally:"), (sp.name, ), msg)
+                        return
+                    if sp.local_deleted and not remote_change:
+                        if direction == DOWN:
+                            return
+                        yield remote.delete_page_pre(sp.remote_name, sp.remote_rev, local_full_iwid)
+                        msg = remote.delete_page_post(yielder.fetch_result())
+                        if not msg:
+                            self.log_status(ActionClass.INFO, _("Deleted page %s remotely."), (sp.name, ))
+                        else:
+                            self.log_status(ActionClass.ERROR, _("Error while deleting page %s remotely:"), (sp.name, ), msg)
+                        return
+                    if sp.local_mime_type != MIMETYPE_MOIN and not (local_change ^ remote_change):
+                        self.log_status(ActionClass.WARN, _("The item %s cannot be merged but was changed in both wikis. Please delete it in one of both wikis and try again."), (sp.name, ))
+                        return
+                    if sp.local_mime_type != sp.remote_mime_type:
+                        self.log_status(ActionClass.WARN, _("The item %s has different mime types in both wikis and cannot be merged. Please delete it in one of both wikis or unify the mime type, and try again."), (sp.name, ))
+                        return
+                    if newest_tag.normalised_name != sp.name:
+                        self.log_status(ActionClass.WARN, _("The item %s was renamed locally. This is not implemented yet. Therefore the full syncronisation history is lost for this page."), (sp.name, )) # XXX implement renames
+                    else:
+                        normalised_name = newest_tag.normalised_name
+                        local_rev = newest_tag.current_rev
+                        remote_rev = newest_tag.remote_rev
+                        old_contents = Page(self.request, local_pagename, rev=newest_tag.current_rev).get_raw_body_str() # YYY direct access
+                else:
+                    if (sp.local_deleted and not sp.remote_rev) or (
+                        sp.remote_deleted and not sp.local_rev):
+                        return
 
-            try:
+                self.log_status(ActionClass.INFO, _("Synchronising page %s with remote page %s ..."), (local_pagename, sp.remote_name))
+
+                if direction == DOWN:
+                    remote_rev = None # always fetch the full page, ignore remote conflict check
+                    patch_base_contents = ""
+                else:
+                    patch_base_contents = old_contents
+
+                if remote_rev != sp.remote_rev:
+                    if sp.remote_deleted: # ignore remote changes
+                        current_remote_rev = sp.remote_rev
+                        is_remote_conflict = False
+                        diff = None
+                        self.log_status(ActionClass.WARN, _("The page %s was deleted remotely but changed locally."), (sp.name, ))
+                    else:
+                        yield remote.get_diff_pre(sp.remote_name, remote_rev, None, normalised_name)
+                        diff_result = remote.get_diff_post(yielder.fetch_result())
+                        if diff_result is None:
+                            self.log_status(ActionClass.ERROR, _("The page %s could not be synced. The remote page was renamed. This is not supported yet. You may want to delete one of the pages to get it synced."), (sp.remote_name, ))
+                            return
+                        is_remote_conflict = diff_result["conflict"]
+                        assert diff_result["diffversion"] == 1
+                        diff = diff_result["diff"]
+                        current_remote_rev = diff_result["current"]
+                else:
+                    current_remote_rev = remote_rev
+                    if sp.local_mime_type == MIMETYPE_MOIN:
+                        is_remote_conflict = wikiutil.containsConflictMarker(old_contents.decode("utf-8"))
+                    else:
+                        is_remote_conflict = NotImplemented
+                    diff = None
+
+                # do not sync if the conflict is remote and local, or if it is local
+                # and the page has never been syncronised
+                if (sp.local_mime_type == MIMETYPE_MOIN and wikiutil.containsConflictMarker(current_page.get_raw_body()) # YYY direct access
+                    and (remote_rev is None or is_remote_conflict)):
+                    self.log_status(ActionClass.WARN, _("Skipped page %s because of a locally or remotely unresolved conflict."), (local_pagename, ))
+                    return
+
+                if remote_rev is None and direction == BOTH:
+                    self.log_status(ActionClass.INFO, _("This is the first synchronisation between the local and the remote wiki for the page %s."), (sp.name, ))
+
+                if sp.remote_deleted:
+                    remote_contents = ""
+                elif diff is None:
+                    remote_contents = old_contents
+                else:
+                    remote_contents = patch(patch_base_contents, decompress(diff))
+
+                if sp.local_mime_type == MIMETYPE_MOIN:
+                    remote_contents_unicode = remote_contents.decode("utf-8")
+                    # here, the actual 3-way merge happens
+                    merged_text = diff3.text_merge(old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), 1, *conflict_markers) # YYY direct access
+                    if debug:
+                        self.log_status(ActionClass.INFO, raw_suffix="Merging %r, %r and %r into %r" % (old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), merged_text))
+                    merged_text_raw = merged_text.encode("utf-8")
+                else:
+                    if diff is None:
+                        merged_text_raw = remote_contents
+                    else:
+                        merged_text_raw = current_page.get_raw_body_str() # YYY direct access
+
+                diff = textdiff(remote_contents, merged_text_raw)
+                if debug:
+                    self.log_status(ActionClass.INFO, raw_suffix="Diff against %r" % remote_contents)
+
+                # XXX upgrade to write lock
+                try:
+                    local_change_done = True
+                    current_page.saveText(merged_text, sp.local_rev or 0, comment=comment) # YYY direct access
+                except PageEditor.Unchanged:
+                    local_change_done = False
+                except PageEditor.EditConflict:
+                    local_change_done = False
+                    assert False, "You stumbled on a problem with the current storage system - I cannot lock pages"
+
+                new_local_rev = current_page.get_real_rev() # YYY direct access
+
+                def rollback_local_change(): # YYY direct local access
+                    rev = new_local_rev - 1
+                    revstr = '%08d' % rev
+                    oldpg = Page(self.request, sp.local_name, rev=rev)
+                    pg = PageEditor(self.request, sp.local_name)
+                    try:
+                        savemsg = pg.saveText(oldpg.get_raw_body(), 0, comment=u"Wikisync rollback", extra=revstr, action="SAVE/REVERT")
+                    except PageEditor.Unchanged:
+                        pass
+                    return sp.local_name
+
+                if local_change_done:
+                    self.register_rollback(rollback_local_change)
+
                 if direction == BOTH:
+                    yield remote.merge_diff_pre(sp.remote_name, compress(diff), new_local_rev, current_remote_rev, current_remote_rev, local_full_iwid, sp.name)
                     try:
-                        very_current_remote_rev = remote.merge_diff(sp.remote_name, compress(diff), new_local_rev, current_remote_rev, current_remote_rev, local_full_iwid, sp.name)
+                        very_current_remote_rev = remote.merge_diff_post(yielder.fetch_result())
                     except NotAllowedException:
                         self.log_status(ActionClass.ERROR, _("The page %s could not be merged because you are not allowed to modify the page in the remote wiki."), (sp.name, ))
                         return
                 else:
                     very_current_remote_rev = current_remote_rev
 
-                local_change_done = False # changes are committed remotely, all is fine
-            finally:
                 if local_change_done:
-                    rollback_local_change()
-
-            tags.add(remote_wiki=remote_full_iwid, remote_rev=very_current_remote_rev, current_rev=new_local_rev, direction=direction, normalised_name=sp.name)
+                    self.remove_rollback(rollback_local_change)
 
-            if sp.local_mime_type != MIMETYPE_MOIN or not wikiutil.containsConflictMarker(merged_text):
-                self.log_status(ActionClass.INFO, _("Page %s successfully merged."), (sp.name, ))
-            else:
-                self.log_status(ActionClass.WARN, _("Page %s merged with conflicts."), (sp.name, ))
+                tags.add(remote_wiki=remote_full_iwid, remote_rev=very_current_remote_rev, current_rev=new_local_rev, direction=direction, normalised_name=sp.name)
 
-            # XXX release lock
+                if sp.local_mime_type != MIMETYPE_MOIN or not wikiutil.containsConflictMarker(merged_text):
+                    self.log_status(ActionClass.INFO, _("Page %s successfully merged."), (sp.name, ))
+                elif is_remote_conflict:
+                    self.log_status(ActionClass.WARN, _("Page %s contains conflicts that were introduced on the remote side."), (sp.name, ))
+                else:
+                    self.log_status(ActionClass.WARN, _("Page %s merged with conflicts."), (sp.name, ))
 
-        for sp in m_pages:
-            handle_page(sp)
+                # XXX release lock
+
+        rpc_aggregator.scheduler(remote.create_multicall_object, handle_page, m_pages, 8, remote.prepare_multicall)
 
 
 def execute(pagename, request):
--- a/MoinMoin/macro/NewPage.py	Thu Aug 24 17:14:26 2006 +0200
+++ b/MoinMoin/macro/NewPage.py	Thu Aug 24 19:37:25 2006 +0200
@@ -52,11 +52,11 @@
     def getArgs(self, argstr):
         """ Temporary function until Oliver Graf args parser is finished
 
-        @param string: string from the wiki markup [[NewPage(string)]]
+        @param argstr: string from the wiki markup [[NewPage(string)]]
         @rtype: dict
         @return: dictionary with macro options
         """
-        if not string:
+        if not argstr:
             return {}
         args = [s.strip() for s in argstr.split(',')]
         args = dict(zip(self.arguments, args))
@@ -78,6 +78,8 @@
 
         if parent == '@ME' and self.request.user.valid:
             parent = self.request.user.name
+        if parent == '@SELF':
+            parent = f.page.page_name
 
         requires_input = '%s' in nametemplate
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/rpc_aggregator.py	Thu Aug 24 19:37:25 2006 +0200
@@ -0,0 +1,117 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - RPC Aggregator
+    
+    Aggregates RPC calls into MultiCall batches in order to increase
+    the speed.
+
+    @copyright: 2006 by MoinMoin:AlexanderSchremmer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import xmlrpclib
+INVALID = object()
+
+class RPCYielder(object):
+    """ If you want to have a batchable function, you need
+    to inherit from this class and define a method "run" that
+    takes exactly one argument.
+    This method has to be a generator that yields (func, arg)
+    tuples whereas func is the RPC method name (str).
+    You can fetch the calls by calling fetch_call(),
+    then you have to return the result by calling set_result(res).
+    """
+
+    def __init__(self, arg, raise_fault=False):
+        self._comm_slot = [INVALID]
+        self.raise_fault = raise_fault
+        self._gen = self.run(arg)
+
+    def fetch_call(self):
+        try:
+            next_item = self._gen.next()
+        except StopIteration:
+            return None
+        return next_item
+
+    def set_result(self, result):
+        self._comm_slot[0] = result
+
+    def fetch_result(self):
+        result = self._comm_slot[0]
+        try:
+            if result is INVALID:
+                return RuntimeError("Invalid state, there is no result to fetch.")
+            if self.raise_fault and isinstance(result, xmlrpclib.Fault):
+                raise result
+            else:
+                return result
+        finally:
+            self._comm_slot[0] = INVALID
+
+    def run(self, arg):
+        return NotImplementedError
+
+
+def scheduler(multicall_func, handler, args, max_calls=10, prepare_multicall_func=None):
+    # all generator (or better, RPCYielder) instances
+    gens = []
+    # those instances that have to be queried in the next step again
+    gens_todo = []
+    # pending calls, stored as tuples: (generator, (funcname, arg))
+    call_list = []
+
+    # instantiate generators
+    for arg in args:
+        gens.append(handler(arg))
+    # schedule generators
+    while gens:
+        for gen in gens:
+            if len(call_list) > max_calls:
+                gens_todo.append(gen)
+                continue
+            call = gen.fetch_call()
+            if call is not None:
+                call_list.append((gen, call))
+                gens_todo.append(gen)
+        if call_list:
+            if prepare_multicall_func is not None:
+                pre_calls = [(RPCYielder(0), x) for x in prepare_multicall_func()]
+                call_list = pre_calls + call_list
+
+            m = multicall_func()
+            gens_result = [] # generators that will get a result
+            for gen, (func, args) in call_list:
+                gens_result.append(gen)
+                getattr(m, func)(*args) # register call
+            result = iter(m()) # execute multicall
+            for gen in gens_result:
+                try:
+                    item = result.next()
+                except xmlrpclib.Fault, e:
+                    # this exception is reraised by the RPCYielder
+                    item = e
+                gen.set_result(item)
+            call_list = []
+        gens = gens_todo
+        gens_todo = []
+
+
+def scheduler_simple(multicall_func, handler, args):
+    for arg in args:
+        cur_handler = handler(arg)
+        while 1:
+            call = cur_handler.fetch_call()
+            if call is not None:
+                func, arg = call
+                m = multicall_func()
+                getattr(m, func)(arg) # register call
+                result = iter(m()) # execute multicall
+                try:
+                    item = result.next()
+                except xmlrpclib.Fault, e:
+                    # this exception is reraised by the RPCYielder
+                    item = e
+                cur_handler.set_result(item)
+            else:
+                break
--- a/MoinMoin/wikisync.py	Thu Aug 24 17:14:26 2006 +0200
+++ b/MoinMoin/wikisync.py	Thu Aug 24 19:37:25 2006 +0200
@@ -86,7 +86,7 @@
 
     def __eq__(self, other):
         if not isinstance(other, SyncPage):
-            return false
+            return False
         return self.name == other.name
 
     def add_missing_pagename(self, local, remote):
@@ -210,55 +210,56 @@
         return xmlrpclib.ServerProxy(self.xmlrpc_url, allow_none=True, verbose=self.verbose)
 
     # Public methods
-    def get_diff(self, pagename, from_rev, to_rev, n_name=None):
+    def get_diff_pre(self, pagename, from_rev, to_rev, n_name=None):
         """ Returns the binary diff of the remote page named pagename, given
-            from_rev and to_rev. """
-        try:
-            if self.token:
-                m = MultiCall(self.connection)
-                m.applyAuthToken(self.token)
-                m.getDiff(pagename, from_rev, to_rev, n_name)
-                tokres, result = m()
-            else:
-                result = self.connection.getDiff(pagename, from_rev, to_rev, n_name)
-        except xmlrpclib.Fault, e:
-            if e.faultCode == "INVALID_TAG":
+            from_rev and to_rev. Generates the call. """
+        return "getDiff", (pagename, from_rev, to_rev, n_name)
+
+    def get_diff_post(self, value):
+        """ Processes the return value of the call generated by get_diff_pre. """
+        if isinstance(value, xmlrpclib.Fault):
+            if value.faultCode == "INVALID_TAG":
                 return None
-            raise
-        result["diff"] = str(result["diff"]) # unmarshal Binary object
+            raise value
+        value["diff"] = str(value["diff"]) # unmarshal Binary object
+        return value
+
+    def merge_diff_pre(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name, n_name):
+        """ Merges the diff into the page on the remote side. Generates the call. """
+        return "mergeDiff", (pagename, xmlrpclib.Binary(diff), local_rev, delta_remote_rev, last_remote_rev, interwiki_name, n_name)
+
+    def merge_diff_post(self, result):
+        """ Processes the return value of the call generated by merge_diff_pre.  """
+        if isinstance(result, xmlrpclib.Fault):
+            if result.faultCode == "NOT_ALLOWED":
+                raise NotAllowedException
+            raise result
         return result
 
-    def merge_diff(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name, n_name):
-        """ Merges the diff into the page on the remote side. """
-        try:
-            if self.token:
-                m = MultiCall(self.connection)
-                m.applyAuthToken(self.token)
-                m.mergeDiff(pagename, xmlrpclib.Binary(diff), local_rev, delta_remote_rev, last_remote_rev, interwiki_name, n_name)
-                tokres, result = m()
-            else:
-                result = self.connection.mergeDiff(pagename, xmlrpclib.Binary(diff), local_rev, delta_remote_rev, last_remote_rev, interwiki_name, n_name)
-        except xmlrpclib.Fault, e:
-            if e.faultCode == "NOT_ALLOWED":
-                raise NotAllowedException
-            raise
-        return result
+    def delete_page_pre(self, pagename, last_remote_rev, interwiki_name):
+        """ Deletes a remote page. Generates the call. """
+        return "mergeDiff", (pagename, None, None, None, last_remote_rev, interwiki_name, None)
 
-    def delete_page(self, pagename, last_remote_rev, interwiki_name):
-        try:
-            if self.token:
-                m = MultiCall(self.connection)
-                m.applyAuthToken(self.token)
-                m.mergeDiff(pagename, None, None, None, last_remote_rev, interwiki_name, None)
-                tokres, result = m()
-            else:
-                result = self.connection.mergeDiff(pagename, None, None, None, last_remote_rev, interwiki_name, None)
-        except xmlrpclib.Fault, e:
-            if e.faultCode == "NOT_ALLOWED":
-                return e.faultString
-            raise
+    def delete_page_post(self, result):
+        """ Processes the return value of the call generated by delete_page_pre. """
+        if isinstance(result, xmlrpclib.Fault):
+            if result.faultCode == "NOT_ALLOWED":
+                return result.faultString
+            raise result
         return ""
 
+    def create_multicall_object(self):
+        """ Generates an object that can be used like a MultiCall instance. """
+        return MultiCall(self.connection)
+
+    def prepare_multicall(self):
+        """ Can be used to return initial calls that e.g. authenticate the user.
+            @return: [(funcname, (arg,+)*]
+        """
+        if self.token:
+            return [("applyAuthToken", (self.token, ))]
+        return []
+
     # Methods implementing the RemoteWiki interface
 
     def get_interwiki_name(self):
--- a/docs/CHANGES	Thu Aug 24 17:14:26 2006 +0200
+++ b/docs/CHANGES	Thu Aug 24 19:37:25 2006 +0200
@@ -187,9 +187,10 @@
     * The i18n system no loads *.po files directly (no *.py or *.mo any more)
       and caches the results (farm wide cache/i18n/*).
     * added the diff parser from ParserMarket, thanks to Emilio Lopes, Fabien
-      Ninoles and Jrgen Hermann.
+      Ninoles and Jürgen Hermann.
     * Added support for "304 not modified" response header for AttachFile get
       and rss_rc actions - faster, less traffic, less load.
+    * Added support for @SELF to the NewPage macro.
 
   Bugfixes:
     * on action "info" page, "revert" link will not be displayed for empty page
@@ -214,6 +215,7 @@
     * Fixed http header output.
     * Fixed request.cfg corruption in the fckdialog code that could lead
       to e.g. stalled servers (thanks to David Linke)
+    * Fixed typo in NewPage that lead to a NameError.
 
   Other changes:
     * we use (again) the same browser compatibility check as FCKeditor uses
@@ -934,7 +936,7 @@
 
   International support:    
     * mail_from can be now a unicode name-address 
-      e.g u'Jrgen wiki <noreply@jhwiki.org>'
+      e.g u'Jürgen wiki <noreply@jhwiki.org>'
 
   Theme changes:
     * logo_string is now should be really only the logo (img).
@@ -2700,7 +2702,7 @@
       there before a new version is written to disk
     * Removed the "Reset" button from EditPage
     * Added "Reduce editor size" link
-    * Added Latin-1 WikiNames (JrgenHermann ;)
+    * Added Latin-1 WikiNames (JürgenHermann ;)
     * Speeded up RecentChanges by looking up hostnames ONCE while saving
     * Show at most 14 (distinct) days in RecentChanges
     * Added icons for common functions, at the top of the page
--- a/docs/CHANGES.aschremmer	Thu Aug 24 17:14:26 2006 +0200
+++ b/docs/CHANGES.aschremmer	Thu Aug 24 19:37:25 2006 +0200
@@ -156,3 +156,10 @@
 2006-08-15: entry missing
 2006-08-16: entry missing
 2006-08-17: entry missing
+2006-08-18: entry missing
+2006-08-19: entry missing
+2006-08-20: entry missing
+2006-08-21: entry missing
+SOC END
+
+