Merge lp:~verterok/ubuntuone-client/dbus-iface-docs into lp:ubuntuone-client

Proposed by Guillermo Gonzalez
Status: Merged
Approved by: Rick McBride
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~verterok/ubuntuone-client/dbus-iface-docs
Merge into: lp:ubuntuone-client
Diff against target: 408 lines (+305/-32)
6 files modified
.bzrignore (+1/-0)
Makefile.am (+4/-0)
contrib/dbus-docs (+193/-0)
contrib/dbus_util.py (+72/-0)
contrib/test (+4/-32)
ubuntuone/syncdaemon/dbus_interface.py (+31/-0)
To merge this branch: bzr merge lp:~verterok/ubuntuone-client/dbus-iface-docs
Reviewer Review Type Date Requested Status
Rick McBride (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+16259@code.launchpad.net

Commit message

export method and signal docstrings via DBus Introspect, add script to generate a simple text file with SyncDaemon DBus API

To post a comment you must log in.
Revision history for this message
Guillermo Gonzalez (verterok) wrote :

This branch adds a script to generate simple text file with the syncdaemon DBus API description.
In order to include the method docstrings I overrided a dbus-python magic method in the parent class of the DBus exposed objects to add an extra <docstring> tag to the Introspect xml, so we can get all the information directly from DBus
Also adds a new make target: 'docs'.

292. By Guillermo Gonzalez

remove commented code

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Looks good, tests pass.

review: Approve
Revision history for this message
Rick McBride (rmcbride) wrote :

Very nice! neat diagram too.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2009-07-15 21:47:57 +0000
3+++ .bzrignore 2009-12-16 20:33:18 +0000
4@@ -42,3 +42,4 @@
5 icons
6 storageprotocol
7 clientdefs.py
8+docs/syncdaemon_dbus_api.txt
9
10=== modified file 'Makefile.am'
11--- Makefile.am 2009-09-25 18:48:03 +0000
12+++ Makefile.am 2009-12-16 20:33:18 +0000
13@@ -62,6 +62,10 @@
14 fi
15 rm -rf _trial_temp
16
17+docs: protocol Makefile
18+ PYTHONPATH="$(PYTHONPATH)" $(PYTHON) $(srcdir)/contrib/dbus-docs
19+
20+
21 protocol: ubuntuone Makefile
22 PROTOCOL="ubuntuone/storageprotocol"; \
23 if [ ! -d $(USP_PATH) ]; then \
24
25=== added file 'contrib/dbus-docs'
26--- contrib/dbus-docs 1970-01-01 00:00:00 +0000
27+++ contrib/dbus-docs 2009-12-16 20:33:18 +0000
28@@ -0,0 +1,193 @@
29+#!/usr/bin/env python
30+#
31+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
32+#
33+# Copyright 2009 Canonical Ltd.
34+#
35+# This program is free software: you can redistribute it and/or modify it
36+# under the terms of the GNU General Public License version 3, as published
37+# by the Free Software Foundation.
38+#
39+# This program is distributed in the hope that it will be useful, but
40+# WITHOUT ANY WARRANTY; without even the implied warranties of
41+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
42+# PURPOSE. See the GNU General Public License for more details.
43+#
44+# You should have received a copy of the GNU General Public License along
45+# with this program. If not, see <http://www.gnu.org/licenses/>.
46+
47+from twisted.internet import glib2reactor
48+glib2reactor.install()
49+
50+import sys
51+import os
52+import signal
53+import dbus
54+import subprocess
55+import shutil
56+
57+from xml.etree import ElementTree
58+
59+sys.path.insert(0, os.path.abspath("."))
60+
61+from contrib.dbus_util import DBusRunner
62+from contrib.testing.testcase import (
63+ FakeMain,
64+ DBusGMainLoop,
65+ DBusInterface,
66+ FakeNetworkManager,
67+)
68+
69+from ubuntuone.syncdaemon.dbus_interface import (
70+ DBUS_IFACE_SYNC_NAME,
71+ DBUS_IFACE_STATUS_NAME,
72+ DBUS_IFACE_EVENTS_NAME,
73+ DBUS_IFACE_FS_NAME,
74+ DBUS_IFACE_SHARES_NAME,
75+ DBUS_IFACE_CONFIG_NAME,
76+)
77+from ubuntuone.syncdaemon.tools import DBusClient
78+from twisted.internet import reactor, defer
79+
80+
81+iface_path = ((DBUS_IFACE_SYNC_NAME, '/'), (DBUS_IFACE_CONFIG_NAME, '/config'),
82+ (DBUS_IFACE_EVENTS_NAME, '/events'),
83+ (DBUS_IFACE_FS_NAME, '/filesystem'),
84+ (DBUS_IFACE_SHARES_NAME, '/shares'),
85+ (DBUS_IFACE_STATUS_NAME, '/status'))
86+
87+
88+def parse_introspect_data(xml):
89+ """Parse the xml returned by Introspect and returns a dict"""
90+ info = dict()
91+ e = ElementTree.fromstring(xml)
92+ for c in e.findall('interface'):
93+ # ignore other interfaces
94+ if not c.attrib['name'].startswith('com.ubuntuone'):
95+ continue
96+ iface_name = c.attrib['name']
97+ info[iface_name] = dict()
98+ # methods
99+ methods = dict()
100+ for method in c.findall('method'):
101+ meth_name = method.attrib['name']
102+ args = []
103+ for arg in method.findall('arg'):
104+ dir = arg.attrib['direction']
105+ type = arg.attrib['type']
106+ if 'name' in arg.attrib:
107+ name = arg.attrib['name']
108+ args.append((type, dir, name))
109+ else:
110+ args.append((type, dir))
111+ docstrings = method.findall('docstring')
112+ docstring = docstrings[0].text if docstrings else 'No docstring'
113+ methods[meth_name] = dict(args=args, docstring=docstring)
114+ info[iface_name]['methods'] = methods
115+ # signals
116+ signals = dict()
117+ for signal in c.findall('signal'):
118+ sig_name = signal.attrib['name']
119+ args = []
120+ for arg in signal.findall('arg'):
121+ type = arg.attrib['type']
122+ name = arg.attrib['name']
123+ args.append((type, name))
124+ docstrings = signal.findall('docstring')
125+ docstring = docstrings[0].text if docstrings else 'No docstring'
126+ signals[sig_name] = dict(args=args, docstring=docstring)
127+ info[iface_name]['signals'] = signals
128+ return info
129+
130+
131+def get_info(path):
132+ """Get all the introspectable info from 'path'"""
133+ d = defer.Deferred()
134+ client = DBusClient(bus, path, 'org.freedesktop.DBus.Introspectable')
135+ client.call_method('Introspect', reply_handler=d.callback,
136+ error_handler=d.errback)
137+ return d
138+
139+
140+def dump_to_stream(info_by_path, stream):
141+ print >>stream, "SyncDaemon DBus API\n"
142+ for path, interfaces in info_by_path.items():
143+ print >>stream, "Object path: %s" % path
144+ for iface_name, kinds in interfaces.items():
145+ print >>stream, " Interface: %s" % iface_name
146+ print >>stream, " Methods:"
147+ for meth_name, val in kinds['methods'].items():
148+ in_args = ','.join([arg[2] + '=' + arg[0] for arg in
149+ val['args'] if arg[1] == 'in'])
150+ out_args = ','.join([arg[0] for arg in val['args']
151+ if arg[1] == 'out'])
152+ if out_args and in_args:
153+ print >>stream, " %s(%s) -> %s" % (meth_name, in_args,
154+ out_args)
155+ elif in_args:
156+ print >>stream, " %s(%s)" % (meth_name, in_args)
157+ else:
158+ print >>stream, " %s()" % meth_name
159+ print >>stream, " %s\n" % val['docstring']
160+ print >>stream, " Signals:"
161+ for signal_name, val in kinds['signals'].items():
162+ in_args = ','.join([arg[1] + '=' + arg[0] \
163+ for arg in val['args']])
164+ if in_args:
165+ print >>stream, " %s(%s)" % (signal_name, in_args)
166+ else:
167+ print >>stream, " %s(%s)" % signal_name
168+ print >>stream, " %s\n" % val['docstring']
169+
170+
171+@defer.inlineCallbacks
172+def main(bus):
173+ """Entry point"""
174+ info_by_path = dict()
175+ for iface, path in iface_path:
176+ xml = yield get_info(path)
177+ obj_info = parse_introspect_data(xml)
178+ info_by_path[path] = obj_info
179+ dest_file = os.path.join(os.getcwd(), 'docs', 'syncdaemon_dbus_api.txt')
180+ with open(dest_file, 'w') as f:
181+ dump_to_stream(info_by_path, f)
182+ reactor.stop()
183+
184+
185+def start_syncdaemon(tmp_dir):
186+ """Starts a syncdaemon instance just like the one used in the test suite"""
187+ xdg_cache = os.path.join(tmp_dir, 'xdg_cache')
188+ data_dir = os.path.join(xdg_cache, 'data')
189+ partials_dir = os.path.join(xdg_cache, 'partials')
190+ root_dir = os.path.join(tmp_dir, 'root')
191+ shares_dir = os.path.join(tmp_dir, 'shares')
192+ main = FakeMain(root_dir, shares_dir, data_dir, partials_dir)
193+ loop = DBusGMainLoop(set_as_default=True)
194+ bus = dbus.bus.BusConnection(mainloop=loop)
195+ nm = FakeNetworkManager(bus)
196+ dbus_iface = DBusInterface(bus, main, system_bus=bus)
197+ main.dbus_iface = dbus_iface
198+ return main, bus
199+
200+
201+if __name__ == '__main__':
202+ dbus_runner = DBusRunner()
203+ dbus_runner.startDBus()
204+ tmp_dir = os.path.join(os.getcwd(), 'tmp')
205+ try:
206+
207+ m, bus = start_syncdaemon(tmp_dir)
208+ try:
209+ reactor.callWhenRunning(main, bus)
210+ reactor.run()
211+ finally:
212+ m.quit()
213+ finally:
214+ dbus_runner.stopDBus()
215+ # remove the tmp dir
216+ os.chmod(tmp_dir, 0777)
217+ for dirpath, dirs, _ in os.walk(tmp_dir):
218+ for dir in dirs:
219+ os.chmod(os.path.join(dirpath, dir), 0777)
220+ shutil.rmtree(tmp_dir)
221+
222
223=== added file 'contrib/dbus_util.py'
224--- contrib/dbus_util.py 1970-01-01 00:00:00 +0000
225+++ contrib/dbus_util.py 2009-12-16 20:33:18 +0000
226@@ -0,0 +1,72 @@
227+#
228+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
229+#
230+# Copyright 2009 Canonical Ltd.
231+#
232+# This program is free software: you can redistribute it and/or modify it
233+# under the terms of the GNU General Public License version 3, as published
234+# by the Free Software Foundation.
235+#
236+# This program is distributed in the hope that it will be useful, but
237+# WITHOUT ANY WARRANTY; without even the implied warranties of
238+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
239+# PURPOSE. See the GNU General Public License for more details.
240+#
241+# You should have received a copy of the GNU General Public License along
242+# with this program. If not, see <http://www.gnu.org/licenses/>.
243+
244+import os
245+import signal
246+import subprocess
247+
248+from distutils.spawn import find_executable
249+
250+
251+class DBusLaunchError(Exception):
252+ """Error while launching dbus-daemon"""
253+ pass
254+
255+class NotFoundError(Exception):
256+ """Not found error"""
257+ pass
258+
259+
260+class DBusRunner(object):
261+
262+ def __init__(self):
263+ self.dbus_address = None
264+ self.dbus_pid = None
265+ self.running = False
266+
267+ def startDBus(self):
268+ """Start our own session bus daemon for testing."""
269+ dbus = find_executable("dbus-daemon")
270+ if not dbus:
271+ raise NotFoundError("dbus-daemon was not found.")
272+
273+ config_file = os.path.join(os.getcwd(), "contrib", "testing",
274+ "dbus-session.conf")
275+ dbus_args = ["--fork",
276+ "--config-file=" + config_file,
277+ "--print-address=1",
278+ "--print-pid=2"]
279+ p = subprocess.Popen([dbus] + dbus_args,
280+ bufsize=4096, stdout=subprocess.PIPE,
281+ stderr=subprocess.PIPE)
282+
283+ self.dbus_address = "".join(p.stdout.readlines()).strip()
284+ self.dbus_pid = int("".join(p.stderr.readlines()).strip())
285+
286+ if self.dbus_address != "":
287+ os.environ["DBUS_SESSION_BUS_ADDRESS"] = self.dbus_address
288+ else:
289+ os.kill(self.dbus_pid, signal.SIGKILL)
290+ raise DBusLaunchError("There was a problem launching dbus-daemon.")
291+ self.running = True
292+
293+ def stopDBus(self):
294+ """Stop our DBus session bus daemon."""
295+ del os.environ["DBUS_SESSION_BUS_ADDRESS"]
296+ os.kill(self.dbus_pid, signal.SIGKILL)
297+ self.running = False
298+
299
300=== modified file 'contrib/test'
301--- contrib/test 2009-11-06 19:31:17 +0000
302+++ contrib/test 2009-12-16 20:33:18 +0000
303@@ -66,36 +66,6 @@
304 suite.addTests(module_suite)
305 return suite
306
307- def startDBus(self):
308- """Start our own session bus daemon for testing."""
309- dbus = find_executable("dbus-daemon")
310- if not dbus:
311- raise NotFoundError("dbus-daemon was not found.")
312-
313- config_file = os.path.join(os.getcwd(), "contrib", "testing",
314- "dbus-session.conf")
315- dbus_args = ["--fork",
316- "--config-file=" + config_file,
317- "--print-address=1",
318- "--print-pid=2"]
319- p = subprocess.Popen([dbus] + dbus_args,
320- bufsize=4096, stdout=subprocess.PIPE,
321- stderr=subprocess.PIPE)
322-
323- self.dbus_address = "".join(p.stdout.readlines()).strip()
324- self.dbus_pid = int("".join(p.stderr.readlines()).strip())
325-
326- if self.dbus_address != "":
327- os.environ["DBUS_SESSION_BUS_ADDRESS"] = self.dbus_address
328- else:
329- os.kill(self.dbus_pid, signal.SIGKILL)
330- raise DBusLaunchError("There was a problem launching dbus-daemon.")
331-
332- def stopDBus(self):
333- """Stop our DBus session bus daemon."""
334- del os.environ["DBUS_SESSION_BUS_ADDRESS"]
335- os.kill(self.dbus_pid, signal.SIGKILL)
336-
337 def run(self, testpath, test_pattern=None):
338 """run the tests. """
339 # install the glib2reactor before any import of the reactor to avoid
340@@ -106,7 +76,9 @@
341 from twisted.trial.reporter import TreeReporter
342 from twisted.trial.runner import TrialRunner
343
344- self.startDBus()
345+ from contrib.dbus_util import DBusRunner
346+ dbus_runner = DBusRunner()
347+ dbus_runner.startDBus()
348
349 workingDirectory = os.path.join(os.getcwd(), "_trial_temp", "tmp")
350 runner = TrialRunner(reporterFactory=TreeReporter, realTimeErrors=True,
351@@ -125,7 +97,7 @@
352 result = runner.run(suite)
353 success = result.wasSuccessful()
354 finally:
355- self.stopDBus()
356+ dbus_runner.stopDBus()
357 if not success:
358 sys.exit(1)
359 else:
360
361=== modified file 'ubuntuone/syncdaemon/dbus_interface.py'
362--- ubuntuone/syncdaemon/dbus_interface.py 2009-12-07 20:49:54 +0000
363+++ ubuntuone/syncdaemon/dbus_interface.py 2009-12-16 20:33:18 +0000
364@@ -22,6 +22,7 @@
365
366 from dbus import DBusException
367 from itertools import groupby, chain
368+from xml.etree import ElementTree
369
370 from twisted.internet import defer
371 from twisted.python.failure import Failure
372@@ -90,6 +91,36 @@
373 """ emit's a Error signal. """
374 self.SignalError(signal, extra_args)
375
376+ @classmethod
377+ def _add_docstring(cls, func, reflection_data):
378+ """add <docstring> tag to reflection_data if func.__doc__ isn't None."""
379+ # add docstring element
380+ if getattr(func, '__doc__', None) is not None:
381+
382+ element = ElementTree.fromstring(reflection_data)
383+ doc = element.makeelement('docstring', dict())
384+ data = '<![CDATA[' + func.__doc__ + ']]>'
385+ doc.text = '%s'
386+ element.insert(0, doc)
387+ return ElementTree.tostring(element) % data
388+ else:
389+ return reflection_data
390+
391+ @classmethod
392+ def _reflect_on_method(cls, func):
393+ """override _reflect_on_method to provide an extra <docstring> element
394+ in the xml.
395+ """
396+ reflection_data = dbus.service.Object._reflect_on_method(func)
397+ reflection_data = cls._add_docstring(func, reflection_data)
398+ return reflection_data
399+
400+ @classmethod
401+ def _reflect_on_signal(cls, func):
402+ reflection_data = dbus.service.Object._reflect_on_signal(func)
403+ reflection_data = cls._add_docstring(func, reflection_data)
404+ return reflection_data
405+
406
407 class Status(DBusExposedObject):
408 """ Represent the status of the syncdaemon """

Subscribers

People subscribed via source and target branches