osimons  committed 6aab3ca

[svn r6054] XmlRpcPlugin: Reworked to normalize output, so that any method can just return regular Python types as used in Trac - typically unicode, datetime, None. It simplifies many method implementations by not having to convert timestamps, check for stray `None` objects and similar.

Bumped version as these internal changes may require changes to other plugins that provide xmlrpc methods.

Among other things, it closes #1245. Thanks for report and testing.

  • Participants
  • Parent commits 542178a

Comments (0)

Files changed (7)

File trunk/

-    version='1.0.1',
+    version='1.0.2',
     author='Alec Thomas',

File trunk/tracrpc/

 import types
 import xmlrpclib
 import datetime
-from tracrpc.util import to_datetime
     set = set
         if result is None:
             result = 0
         elif isinstance(result, dict):
-            for key,val in result.iteritems():
-                if isinstance(val, datetime.datetime):
-                    result[key] = to_datetime(val)
-            #pass
+            pass
         elif not isinstance(result, basestring):
             # Try and convert result to a list

File trunk/tracrpc/

 from trac.core import *
 from tracrpc.api import IXMLRPCHandler
-from tracrpc.util import to_datetime
 from import ISearchSource
 from import SearchModule
         self.env.log.debug("Searching with %s" % filters)
         results = []
-        converters = [unicode, unicode, to_datetime, unicode, unicode]         	
         for source in self.search_sources:
             for result in source.get_search_results(req, query, filters):
-                result = [f(v) for f,v in zip(converters, result)]
                                 + result[0]] + list(result[1:]))
         return results

File trunk/tracrpc/

 from trac.attachment import Attachment
 from trac.core import *
 from trac.perm import PermissionCache
-from tracrpc.api import IXMLRPCHandler, expose_rpc
-from tracrpc.util import to_datetime
 import trac.ticket.model as model
 import trac.ticket.query as query
 from trac.ticket.api import TicketSystem
 from trac.ticket.notification import TicketNotifyEmail
 from trac.ticket.web_ui import TicketModule
-from trac.util.datefmt import to_timestamp, utc
+from trac.util.datefmt import to_datetime, to_timestamp, utc
 import genshi
-from datetime import datetime
 import inspect
 import xmlrpclib
 from StringIO import StringIO
+from tracrpc.api import IXMLRPCHandler, expose_rpc
 class TicketRPC(Component):
     """ An interface to Trac's ticketing system. """
     def get(self, req, id):
         """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """
         t = model.Ticket(self.env, id)
-        return (, to_datetime(t.time_created), 
-                to_datetime(t.time_changed), t.values)
+        return (, t.time_created, t.time_changed, t.values)
     def create(self, req, summary, description, attributes = {}, notify=False):
         """ Create a new ticket, returning the ticket ID. """
         t = model.Ticket(self.env)
         t['summary'] = summary
         t['description'] = description
-        t['reporter'] = req.authname or 'anonymous'
+        t['reporter'] = req.authname
         for k, v in attributes.iteritems():
             t[k] = v
         t['status'] = 'new'
     def update(self, req, id, comment, attributes = {}, notify=False):
         """ Update a ticket, returning the new ticket in the same form as
         getTicket(). Requires a valid 'action' in attributes to support workflow. """
-        now =
+        now = to_datetime(None, utc)
         t = model.Ticket(self.env, id)
         if not 'action' in attributes:
             # FIXME: Old, non-restricted update - remove soon!
     def changeLog(self, req, id, when=0):
         t = model.Ticket(self.env, id)
         for date, author, field, old, new, permanent in t.get_changelog(when):
-            yield (to_datetime(date), author, field, old, new, permanent)
+            yield (date, author, field, old, new, permanent)
     # Use existing documentation from Ticket model
     changeLog.__doc__ = inspect.getdoc(model.Ticket.get_changelog)
         """ Lists attachments for a given ticket. Returns (filename,
         description, size, time, author) for each attachment."""
         for t in, 'ticket', ticket):
-            yield (t.filename, t.description or '', t.size, 
-                   to_datetime(,
+            yield (t.filename, t.description, t.size, 
+         ,
     def getAttachment(self, req, ticket, filename):
         """ returns the content of an attachment. """
             except TracError:
         attachment = Attachment(self.env, 'ticket', ticket)
- = req.authname or 'anonymous'
+ = req.authname
         attachment.description = description
         attachment.insert(filename, StringIO(, len(
         return attachment.filename

File trunk/tracrpc/

 from trac.util.datefmt import utc
-def datetime_to_xmlrpc_timestamp(datetime):
-    """ Convert xmlrpclib.DateTime string representation to UNIX timestamp. """
-    return time.mktime(time.strptime('%s UTC' % datetime.value, '%Y%m%dT%H:%M:%S %Z')) - time.timezone
-def to_datetime(dt):
+def to_xmlrpc_datetime(dt):
     """ Convert a datetime.datetime object to a xmlrpclib DateTime object """
     return xmlrpclib.DateTime(dt.utctimetuple())

File trunk/tracrpc/

 import re
 import xmlrpclib
+import datetime
 from pkg_resources import resource_filename
 from trac.core import *
 from import wiki_to_oneliner
 from tracrpc.api import IXMLRPCHandler, XMLRPCSystem
-from tracrpc.util import from_xmlrpc_datetime
+from tracrpc.util import from_xmlrpc_datetime, to_xmlrpc_datetime
 class XMLRPCWeb(Component):
     """ Handle XML-RPC calls from HTTP clients, as well as presenting a list of
         args = self._normalize_input(args)
             result = XMLRPCSystem(self.env).get_method(method)(req, args)
+            self.env.log.debug("RPC(xml) '%s' result: %s" % (method, repr(result)))
+            result = tuple(self._normalize_output(result))
             self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), content_type)
         except xmlrpclib.Fault, e:
         return new_args
+    def _normalize_output(self, result):
+        """ Normalizes and converts output (traversing it):
+        1. None => ''
+        2. datetime => xmlrpclib.DateTime
+        """
+        new_result = []
+        for res in result:
+            if isinstance(res, datetime.datetime):
+                new_result.append(to_xmlrpc_datetime(res))
+            elif res == None:
+                new_result.append('')
+            elif isinstance(res, dict):
+                for key in res.keys():
+                    res[key] = self._normalize_output([res[key]])[0]
+                new_result.append(res)
+            elif isinstance(res, list) or isinstance(res, tuple):
+                new_result.append(self._normalize_output(res))
+            else:
+                new_result.append(res)
+        return new_result
     # ITemplateProvider
     def get_htdocs_dirs(self):
         return []

File trunk/tracrpc/

     from StringIO import StringIO
 import xmlrpclib
 import posixpath
-import time
 from trac.core import *
 from trac.perm import IPermissionRequestor
-from trac.util.datefmt import to_timestamp
+from trac.util.datefmt import to_timestamp, to_datetime, utc
 from import WikiSystem
 from import WikiPage
 from import wiki_to_html
 from trac.attachment import Attachment
 from tracrpc.api import IXMLRPCHandler, expose_rpc
-from tracrpc.util import to_datetime
 class WikiRPC(Component):
     """Superset of the
         yield ('WIKI_VIEW', ((list, str),), self.listLinks)
         yield ('WIKI_VIEW', ((str, str),), self.wikiToHtml)
-    def _page_info(self, name, time, author, version, comment):
-        return dict(name=name, lastModified=xmlrpclib.DateTime(int(time)),
-                    author=author, version=int(version), comment=comment or '')
+    def _page_info(self, name, when, author, version, comment):
+        return dict(name=name, lastModified=to_datetime(when, utc),
+                    author=author, version=int(version), comment=comment)
     def getRecentChanges(self, req, since):
         """ Get list of changed pages since timestamp """
         cursor.execute('SELECT name, max(time), author, version, comment FROM wiki'
                        ' WHERE time >= %s GROUP BY name ORDER BY max(time) DESC', (since,))
         result = []
-        for name, time, author, version, comment in cursor:
-            result.append(self._page_info(name, time, author, version, comment))
+        for name, when, author, version, comment in cursor:
+            result.append(self._page_info(name, when, author, version, comment))
         return result
     def getRPCVersionSupported(self, req):
         page = WikiPage(self.env, pagename, version)
         if page.exists:
             last_update = page.get_history().next()
-            return self._page_info(,
-                                   time.mktime(last_update[1].utctimetuple()),
+            return self._page_info(, last_update[1],
                                    last_update[2], page.version, page.comment)
     def putPage(self, req, pagename, content, attributes):
             except TracError:
         attachment = Attachment(self.env, 'wiki', pagename)
- = req.authname or 'anonymous'
+ = req.authname
         attachment.description = description
         attachment.insert(filename, StringIO(, len(
         return attachment.filename