Merge lp:~robru/gwibber/dispatcher into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1427
Proposed branch: lp:~robru/gwibber/dispatcher
Merge into: lp:~barry/gwibber/py3
Diff against target: 736 lines (+303/-89)
12 files modified
gwibber/gwibber/main.py (+14/-18)
gwibber/gwibber/protocols/flickr.py (+6/-8)
gwibber/gwibber/protocols/twitter.py (+2/-1)
gwibber/gwibber/service/dispatcher.py (+201/-0)
gwibber/gwibber/testing/com.Gwibber.Service.service.in (+3/-0)
gwibber/gwibber/testing/dbus.py (+1/-0)
gwibber/gwibber/tests/test_dbus.py (+39/-22)
gwibber/gwibber/tests/test_flickr.py (+6/-8)
gwibber/gwibber/tests/test_protocols.py (+10/-15)
gwibber/gwibber/tests/test_twitter.py (+1/-1)
gwibber/gwibber/utils/account.py (+9/-2)
gwibber/gwibber/utils/protocol.py (+11/-14)
To merge this branch: bzr merge lp:~robru/gwibber/dispatcher
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+123652@code.launchpad.net

Description of the change

Alright barry, this is the new dispatcher. As far as I can tell, it's about 90% done (not counting the lack of tests). So just watch out for a couple of TODOs in the source (which are well-marked I think), but anything not immediately adjacent to a #TODO comment should be considered complete.

Just need your advice with some integration tests, and then we can pleasantly proceed to the protocol plugin porting process... p. ;-)

To post a comment you must log in.
lp:~robru/gwibber/dispatcher updated
1426. By Robert Bruce Park

Allow Dispatcher.GetAvatarPath to return cached filenames even if offline.

Note that if we are in offline mode, no checks are done to say whether or not
the cache actually contains the file returned by this method, so it is necessary
for consumers of this function to be able to fail gracefully in the event
that the filename returned does not actually exist, or contains corrupted data.

1427. By Robert Bruce Park

Finish implementing Dispatcher.Refresh

Based on reviewing the old plugins it looks like 'receive' is the correct
operation for downloading messages, but this is not actually 100% clear to
me, so this might need to be tweaked.

1428. By Robert Bruce Park

Fix some typos, insert Dispatcher into main.py, and write the first (failing)
test.

1429. By Robert Bruce Park

Invoke super() so that the Dispatcher can actually be called through dbus.

1430. By Robert Bruce Park

Converted services from an array to a class.

This makes it easier to pass references from one service to another's
constructor, cleaning up service instantiation quite a bit.

1431. By Robert Bruce Park

Convert protocol info into a sub-namespace called 'info'.

This was necessary because the primary protocol class namespace is reserved
for callable methods that can be accessed through __call__, so now eg
protocol.version has been renamed to protocol.info.version, etc.

This commit results in the Dispatcher.GetFeatures test passing, because it's
now possible to introspect what features a protocol supports just by looking
at its public methods.

1432. By Robert Bruce Park

Expand test_get_features to check invalid inputs.

1433. By Robert Bruce Park

Call Dispatcher.Refresh periodically from the GLib mainloop.

1434. By Robert Bruce Park

Get refresh timeout interval from GSettings.

1435. By Robert Bruce Park

Drop protocol.info.features as it is redundant; we can introspect what
features are present based on what methods are present on the protocol
instance.

1436. By Robert Bruce Park

Add Dispatcher.online property to enhance readability of the timer_id.

1437. By Robert Bruce Park

Raise NotImplementedErrors instead of AttributeErrors for protocols.

This makes more sense.

1438. By Robert Bruce Park

Rename _manager to protocolmanager because it is actually public.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/main.py'
2--- gwibber/gwibber/main.py 2012-09-07 19:00:21 +0000
3+++ gwibber/gwibber/main.py 2012-09-12 21:17:40 +0000
4@@ -30,6 +30,7 @@
5 from gi.repository import Gio, GLib
6
7 from gwibber.service.connection import ConnectionMonitor
8+from gwibber.service.dispatcher import Dispatcher
9 from gwibber.service.messages import MessageManager
10 from gwibber.service.searches import SearchManager
11 from gwibber.service.shortener import URLShorten
12@@ -42,17 +43,10 @@
13
14 # Logger must be initialized before it can be used.
15 log = None
16-services = []
17 DEFAULT_SQLITE_FILE = os.path.join(
18 GLib.get_user_config_dir(), 'gwibber', 'gwibber.sqlite')
19
20
21-class DispatcherStub:
22- # TODO: Once we have a real dispatcher, this can go away.
23- def refresh(self):
24- pass
25-
26-
27 def main():
28 global log
29 # Initialize command line options.
30@@ -71,21 +65,23 @@
31 # Set up the DBus main loop.
32 DBusGMainLoop(set_as_default=True)
33 loop = GLib.MainLoop()
34- # Keep a reference to the connection monitor for testing purposes.
35- connection_monitor = ConnectionMonitor()
36+
37+ refresh_interval = max(gsettings.get_int('interval'), 5) * 60
38+
39 # Load up the various services. We do it this way so that we retain
40 # references to the service endpoints without pyflakes screaming at us
41 # about unused local variables.
42- services.extend([
43- connection_monitor,
44- MenuManager(DispatcherStub().refresh, loop.quit),
45- MessageManager(database),
46- SearchManager(database),
47- StreamManager(database),
48- URLShorten(gsettings),
49- ])
50+ class services:
51+ connection = ConnectionMonitor()
52+ streams = StreamManager(database)
53+ dispatcher = Dispatcher(loop, streams, refresh_interval)
54+ menus = MenuManager(dispatcher.Refresh, loop.quit)
55+ messages = MessageManager(database)
56+ searches = SearchManager(database)
57+ shorten = URLShorten(gsettings)
58+
59 if args.test:
60- services.append(TestService(database, connection_monitor))
61+ services.test = TestService(database, services.connection)
62 try:
63 log.info('Starting gwibber-service main loop')
64 loop.run()
65
66=== modified file 'gwibber/gwibber/protocols/flickr.py'
67--- gwibber/gwibber/protocols/flickr.py 2012-09-06 16:24:33 +0000
68+++ gwibber/gwibber/protocols/flickr.py 2012-09-12 21:17:40 +0000
69@@ -50,14 +50,12 @@
70
71
72 class Flickr(Base):
73- # Protocol information.
74- name = 'Flickr'
75- version = '1.0'
76- config = ['username', 'color', 'receive_enabled']
77- authtype = 'none'
78- color = '#C31A00'
79- features = ['receive', 'images']
80- default_streams = ['images']
81+ class info:
82+ version = '1.0'
83+ config = ['username', 'color', 'receive_enabled']
84+ authtype = 'none'
85+ color = '#C31A00'
86+ default_streams = ['images']
87
88 def _get_nsid(self):
89 """Get the user's Flickr id.
90
91=== modified file 'gwibber/gwibber/protocols/twitter.py'
92--- gwibber/gwibber/protocols/twitter.py 2012-09-07 19:15:05 +0000
93+++ gwibber/gwibber/protocols/twitter.py 2012-09-12 21:17:40 +0000
94@@ -23,4 +23,5 @@
95
96
97 class Twitter(Base):
98- name = 'Twitter'
99+ class info:
100+ pass
101
102=== added file 'gwibber/gwibber/service/dispatcher.py'
103--- gwibber/gwibber/service/dispatcher.py 1970-01-01 00:00:00 +0000
104+++ gwibber/gwibber/service/dispatcher.py 2012-09-12 21:17:40 +0000
105@@ -0,0 +1,201 @@
106+# Copyright (C) 2012 Canonical Ltd
107+#
108+# This program is free software: you can redistribute it and/or modify
109+# it under the terms of the GNU General Public License version 2 as
110+# published by the Free Software Foundation.
111+#
112+# This program is distributed in the hope that it will be useful,
113+# but WITHOUT ANY WARRANTY; without even the implied warranty of
114+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
115+# GNU General Public License for more details.
116+#
117+# You should have received a copy of the GNU General Public License
118+# along with this program. If not, see <http://www.gnu.org/licenses/>.
119+
120+"""DBus service object for general dispatching of commands."""
121+
122+__all__ = [
123+ 'Dispatcher',
124+ ]
125+
126+
127+import json
128+import dbus
129+import dbus.service
130+import threading
131+
132+import logging
133+log = logging.getLogger('gwibber.service')
134+
135+from gi.repository import GLib
136+
137+from gwibber.utils.account import AccountManager, protocolmanager
138+from gwibber.utils.avatar import Avatar
139+from gwibber.utils.signaler import signaler
140+
141+
142+class Dispatcher(dbus.service.Object):
143+ """This is the primary handler of dbus method calls."""
144+ __dbus_object_path__ = '/com/gwibber/Service'
145+
146+ def __init__(self, mainloop, streams, interval):
147+ self.bus = dbus.SessionBus()
148+ bus_name = dbus.service.BusName('com.Gwibber.Service', bus=self.bus)
149+ super().__init__(bus_name, self.__dbus_object_path__)
150+ self.mainloop = mainloop
151+ self.account_manager = AccountManager(self.Refresh, streams)
152+
153+ self._timer_id = None
154+ def on_connection_online():
155+ if not self._timer_id:
156+ self._timer_id = GLib.timeout_add_seconds(interval, self.Refresh)
157+
158+ def on_connection_offline():
159+ try:
160+ GLib.source_remove(self._timer_id)
161+ except TypeError:
162+ pass
163+ finally:
164+ self._timer_id = None
165+
166+ signaler.add_signal('ConnectionOnline', on_connection_online)
167+ signaler.add_signal('ConnectionOffline', on_connection_offline)
168+ on_connection_online()
169+
170+ @property
171+ def online(self):
172+ return self._timer_id is not None
173+
174+ @dbus.service.method('com.Gwibber.Service')
175+ def Refresh(self):
176+ # TODO this method is too untestable here; split it off somewhere
177+ # else for easier testing, then invoke it from here.
178+ log.debug('Refresh requested')
179+ # Wait for all previous actions to complete before
180+ # starting a load of new ones.
181+ current = threading.current_thread()
182+ for thread in threading.enumerate():
183+ if thread != current:
184+ thread.join()
185+
186+ if not self.online:
187+ return
188+
189+ # account.protocol() starts a new thread for each account present
190+ # and returns immediately. So this method should be quick unless
191+ # there are a bunch of old, incomplete jobs waiting around from
192+ # the last refresh.
193+ for account in self.account_manager.get_all():
194+ try:
195+ account.protocol('receive')
196+ except NotImplementedError:
197+ # If a protocol doesn't support receive then ignore it.
198+ pass
199+
200+ # Always return True, or else GLib mainloop will stop invoking it.
201+ return True
202+
203+ @dbus.service.method('com.Gwibber.Service', in_signature='s')
204+ def UpdateIndicators(self, stream):
205+ """Update counts in messaging indicators.
206+
207+ example:
208+ import dbus
209+ obj = dbus.SessionBus().get_object(
210+ 'com.Gwibber.Service', '/com/gwibber/Service')
211+ service = dbus.Interface(obj, 'com.Gwibber.Service')
212+ service.UpdateIndicators('stream')
213+ """
214+ pass #TODO
215+
216+ @dbus.service.method('com.Gwibber.Service', in_signature='sss')
217+ def Do(self, action, account_id=None, message_id=None):
218+ """Performs a certain operation specific to individual messages.
219+
220+ This is how the client initiates retweeting, liking, unliking, etc.
221+ example:
222+ import dbus
223+ obj = dbus.SessionBus().get_object(
224+ 'com.Gwibber.Service', '/com/gwibber/Service')
225+ service = dbus.Interface(obj, 'com.Gwibber.Service')
226+ service.Do('like', account, message_id)
227+ """
228+ if not self.online:
229+ return
230+ log.debug('{}-ing {}'.format(action, message_id))
231+ account = self.account_manager.get(account_id)
232+ if account is not None:
233+ account.protocol(action, message_id=message_id)
234+
235+ @dbus.service.method('com.Gwibber.Service', in_signature='s')
236+ def SendMessage(self, message):
237+ """Posts a message/status update to all send_enabled accounts.
238+
239+ It takes one argument, which is a message formated as a string.
240+ example:
241+ import dbus
242+ obj = dbus.SessionBus().get_object(
243+ 'com.Gwibber.Service', '/com/gwibber/Service')
244+ service = dbus.Interface(obj, 'com.Gwibber.Service')
245+ service.SendMessage('Your message')
246+ """
247+ if not self.online:
248+ return
249+ for account in self.account_manager.get_all():
250+ if account['send_enabled']:
251+ account.protocol('send', message)
252+
253+ @dbus.service.method('com.Gwibber.Service', out_signature='s')
254+ def GetFeatures(self, protocol_name):
255+ """Returns a list of features supported by service as json string.
256+
257+ example:
258+ import dbus, json
259+ obj = dbus.SessionBus().get_object(
260+ 'com.Gwibber.Service', '/com/gwibber/Service')
261+ service = dbus.Interface(obj, 'com.Gwibber.Service')
262+ features = json.loads(service.GetFeatures('facebook'))
263+ """
264+ # Note: in the case of .get returning None,
265+ # features is set to a harmless empty list.
266+ protocol = protocolmanager.protocols.get(protocol_name)
267+ features = [operation for operation
268+ in dir(protocol)
269+ if operation != 'info' and
270+ not operation.startswith('_')]
271+ return json.dumps(features)
272+
273+ @dbus.service.method('com.Gwibber.Service',
274+ in_signature='s',
275+ out_signature='s')
276+ def GetAvatarPath(self, url):
277+ """Returns the path to the cached avatar image as a string.
278+
279+ example:
280+ import dbus
281+ obj = dbus.SessionBus().get_object(
282+ 'com.Gwibber.Service', '/com/gwibber/Service')
283+ service = dbus.Interface(obj, 'com.Gwibber.Service')
284+ avatar = service.GetAvatar(url)
285+ """
286+ if self.online:
287+ # Download the image to the cache, if necessary.
288+ return Avatar.get_image(url)
289+ else:
290+ # Report the cache location without attempting download.
291+ return Avatar.get_path(url)
292+
293+ @dbus.service.method('com.Gwibber.Service')
294+ def Quit(self):
295+ """Shutdown the service.
296+
297+ example:
298+ import dbus
299+ obj = dbus.SessionBus().get_object(
300+ 'com.Gwibber.Service', '/com/gwibber/Service')
301+ service = dbus.Interface(obj, 'com.Gwibber.Service')
302+ service.Quit()
303+ """
304+ log.info('Gwibber Service is being shutdown')
305+ logging.shutdown()
306+ self.mainloop.quit()
307
308=== added file 'gwibber/gwibber/testing/com.Gwibber.Service.service.in'
309--- gwibber/gwibber/testing/com.Gwibber.Service.service.in 1970-01-01 00:00:00 +0000
310+++ gwibber/gwibber/testing/com.Gwibber.Service.service.in 2012-09-12 21:17:40 +0000
311@@ -0,0 +1,3 @@
312+[D-BUS Service]
313+Name=com.Gwibber.Service
314+Exec={BINDIR}/gwibber-service --database {TMPDIR}/gwibber.db --test
315
316=== modified file 'gwibber/gwibber/testing/dbus.py'
317--- gwibber/gwibber/testing/dbus.py 2012-08-21 22:42:24 +0000
318+++ gwibber/gwibber/testing/dbus.py 2012-09-12 21:17:40 +0000
319@@ -51,6 +51,7 @@
320 'com.Gwibber.Connection',
321 'com.Gwibber.Messages',
322 'com.Gwibber.Searches',
323+ 'com.Gwibber.Service',
324 'com.Gwibber.Streams',
325 'com.Gwibber.Test',
326 'com.Gwibber.URLShorten',
327
328=== modified file 'gwibber/gwibber/tests/test_dbus.py'
329--- gwibber/gwibber/tests/test_dbus.py 2012-09-07 15:20:41 +0000
330+++ gwibber/gwibber/tests/test_dbus.py 2012-09-12 21:17:40 +0000
331@@ -66,19 +66,20 @@
332 _controller.shutdown()
333
334 def setUp(self):
335+ self.sessionbus = dbus.SessionBus()
336 # We have to make sure to remove any rows that the individual tests
337 # may have added, otherwise there are order dependencies.
338 self.stream_ids = []
339 self.search_ids = []
340
341 def tearDown(self):
342- obj = dbus.SessionBus().get_object(
343+ obj = self.sessionbus.get_object(
344 'com.Gwibber.Streams',
345 '/com/gwibber/Streams')
346 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
347 for id in self.stream_ids:
348 iface.Delete(id)
349- obj = dbus.SessionBus().get_object(
350+ obj = self.sessionbus.get_object(
351 'com.Gwibber.Searches',
352 '/com/gwibber/Searches')
353 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
354@@ -86,7 +87,7 @@
355 iface.Delete(id)
356
357 def test_shorten(self):
358- obj = dbus.SessionBus().get_object(
359+ obj = self.sessionbus.get_object(
360 'com.Gwibber.URLShorten',
361 '/com/gwibber/URLShorten')
362 iface = dbus.Interface(obj, 'com.Gwibber.URLShorten')
363@@ -94,14 +95,14 @@
364 self.assertIsInstance(short_url, dbus.String)
365
366 def test_streams_list_empty(self):
367- obj = dbus.SessionBus().get_object(
368+ obj = self.sessionbus.get_object(
369 'com.Gwibber.Streams',
370 '/com/gwibber/Streams')
371 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
372 self.assertEqual(iface.List(), '[]')
373
374 def test_streams_create(self):
375- obj = dbus.SessionBus().get_object(
376+ obj = self.sessionbus.get_object(
377 'com.Gwibber.Streams',
378 '/com/gwibber/Streams')
379 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
380@@ -116,7 +117,7 @@
381 self.assertEqual(iface.List(), '[payload]')
382
383 def test_streams_create_two(self):
384- obj = dbus.SessionBus().get_object(
385+ obj = self.sessionbus.get_object(
386 'com.Gwibber.Streams',
387 '/com/gwibber/Streams')
388 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
389@@ -134,7 +135,7 @@
390 self.assertEqual(iface.List(), '[one, two]')
391
392 def test_streams_delete(self):
393- obj = dbus.SessionBus().get_object(
394+ obj = self.sessionbus.get_object(
395 'com.Gwibber.Streams',
396 '/com/gwibber/Streams')
397 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
398@@ -150,7 +151,7 @@
399 self.assertEqual(iface.List(), '[]')
400
401 def test_streams_get(self):
402- obj = dbus.SessionBus().get_object(
403+ obj = self.sessionbus.get_object(
404 'com.Gwibber.Streams',
405 '/com/gwibber/Streams')
406 iface = dbus.Interface(obj, 'com.Gwibber.Streams')
407@@ -171,7 +172,7 @@
408 # First, insert some data into the messages table. We do this through
409 # the dbus testing interface so that we don't have to worry about
410 # SQLite lock contention.
411- obj = dbus.SessionBus().get_object(
412+ obj = self.sessionbus.get_object(
413 'com.Gwibber.Test',
414 '/com/gwibber/Test')
415 test_iface = dbus.Interface(obj, 'com.Gwibber.Test')
416@@ -179,7 +180,7 @@
417 stored_ids[test_iface.InsertDataIntoMessages('one')] = 'one'
418 stored_ids[test_iface.InsertDataIntoMessages('two')] = 'two'
419 # Now get the data back through the non-testing API.
420- obj = dbus.SessionBus().get_object(
421+ obj = self.sessionbus.get_object(
422 'com.Gwibber.Messages',
423 '/com/gwibber/Messages')
424 iface = dbus.Interface(obj, 'com.Gwibber.Messages')
425@@ -193,7 +194,7 @@
426 # First, insert some data into the messages table. We do this through
427 # the dbus testing interface so that we don't have to worry about
428 # SQLite lock contention.
429- obj = dbus.SessionBus().get_object(
430+ obj = self.sessionbus.get_object(
431 'com.Gwibber.Test',
432 '/com/gwibber/Test')
433 test_iface = dbus.Interface(obj, 'com.Gwibber.Test')
434@@ -201,7 +202,7 @@
435 stored_ids.append(test_iface.InsertDataIntoMessages('one'))
436 stored_ids.append(test_iface.InsertDataIntoMessages('two'))
437 # Now get the data back through the non-testing API.
438- obj = dbus.SessionBus().get_object(
439+ obj = self.sessionbus.get_object(
440 'com.Gwibber.Messages',
441 '/com/gwibber/Messages')
442 iface = dbus.Interface(obj, 'com.Gwibber.Messages')
443@@ -216,7 +217,7 @@
444 # Test whether the network manager is connected or not. This uses the
445 # standard system service, and we can't guarantee its state will be
446 # connected, so just ensure that we get a boolean back.
447- obj = dbus.SessionBus().get_object(
448+ obj = self.sessionbus.get_object(
449 'com.Gwibber.Connection',
450 '/com/gwibber/Connection')
451 iface = dbus.Interface(obj, 'com.Gwibber.Connection')
452@@ -224,7 +225,7 @@
453 self.assertIsInstance(answer, dbus.Boolean)
454
455 def test_connection_internal_signals(self):
456- obj = dbus.SessionBus().get_object(
457+ obj = self.sessionbus.get_object(
458 'com.Gwibber.Test',
459 '/com/gwibber/Test')
460 test_iface = dbus.Interface(obj, 'com.Gwibber.Test')
461@@ -242,14 +243,14 @@
462 # .isConnected().
463
464 def test_searches_list_empty(self):
465- obj = dbus.SessionBus().get_object(
466+ obj = self.sessionbus.get_object(
467 'com.Gwibber.Searches',
468 '/com/gwibber/Searches')
469 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
470 self.assertEqual(iface.List(), '[]')
471
472 def test_searches_create(self):
473- obj = dbus.SessionBus().get_object(
474+ obj = self.sessionbus.get_object(
475 'com.Gwibber.Searches',
476 '/com/gwibber/Searches')
477 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
478@@ -262,7 +263,7 @@
479 self.assertEqual(iface.List(), '[payload]')
480
481 def test_searches_create_two(self):
482- obj = dbus.SessionBus().get_object(
483+ obj = self.sessionbus.get_object(
484 'com.Gwibber.Searches',
485 '/com/gwibber/Searches')
486 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
487@@ -277,7 +278,7 @@
488 self.assertEqual(iface.List(), '[one, two]')
489
490 def test_searches_delete(self):
491- obj = dbus.SessionBus().get_object(
492+ obj = self.sessionbus.get_object(
493 'com.Gwibber.Searches',
494 '/com/gwibber/Searches')
495 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
496@@ -294,7 +295,7 @@
497 # Deleting search data also deletes rows from the messages table where
498 # the transients column matches the id in the storage table. Call
499 # into the test service to set up the data for this.
500- obj = dbus.SessionBus().get_object(
501+ obj = self.sessionbus.get_object(
502 'com.Gwibber.Test',
503 '/com/gwibber/Test')
504 test_iface = dbus.Interface(obj, 'com.Gwibber.Test')
505@@ -303,12 +304,12 @@
506 stored_id_2 = test_iface.InsertSearchTransientData(
507 'bart', 'query2', 'two')
508 # Now there should be two rows in both tables.
509- obj = dbus.SessionBus().get_object(
510+ obj = self.sessionbus.get_object(
511 'com.Gwibber.Searches',
512 '/com/gwibber/Searches')
513 searches_iface = dbus.Interface(obj, 'com.Gwibber.Searches')
514 self.assertEqual(searches_iface.List(), '[one, two]')
515- obj = dbus.SessionBus().get_object(
516+ obj = self.sessionbus.get_object(
517 'com.Gwibber.Messages',
518 '/com/gwibber/Messages')
519 messages_iface = dbus.Interface(obj, 'com.Gwibber.Messages')
520@@ -327,7 +328,7 @@
521 self.assertEqual(messages_iface.Get(stored_id_2), '')
522
523 def test_searches_get(self):
524- obj = dbus.SessionBus().get_object(
525+ obj = self.sessionbus.get_object(
526 'com.Gwibber.Searches',
527 '/com/gwibber/Searches')
528 iface = dbus.Interface(obj, 'com.Gwibber.Searches')
529@@ -340,3 +341,19 @@
530 self.search_ids.append(id)
531
532 # XXX Need tests for the Updated(), Deleted(), and Created() signals.
533+
534+ def test_get_features(self):
535+ obj = self.sessionbus.get_object(
536+ 'com.Gwibber.Service',
537+ '/com/gwibber/Service')
538+ iface = dbus.Interface(obj, 'com.Gwibber.Service')
539+
540+ # TODO Add more cases as more protocols are added.
541+ cases = dict(flickr = ['images'],
542+ base = [],
543+ invalid = [])
544+ for case in cases:
545+ self.assertEqual(
546+ cases[case],
547+ json.loads(
548+ iface.GetFeatures(case)))
549
550=== modified file 'gwibber/gwibber/tests/test_flickr.py'
551--- gwibber/gwibber/tests/test_flickr.py 2012-09-07 19:15:05 +0000
552+++ gwibber/gwibber/tests/test_flickr.py 2012-09-12 21:17:40 +0000
553@@ -52,15 +52,13 @@
554
555 def test_protocol_info(self):
556 # Each protocol carries with it a number of protocol variables.
557- self.assertEqual(self.protocol.name, 'Flickr')
558- self.assertEqual(self.protocol.version, '1.0')
559- self.assertEqual(self.protocol.config,
560+ self.assertEqual(self.protocol.__class__.__name__, 'Flickr')
561+ self.assertEqual(self.protocol.info.version, '1.0')
562+ self.assertEqual(self.protocol.info.config,
563 ['username', 'color', 'receive_enabled'])
564- self.assertEqual(self.protocol.authtype, 'none')
565- self.assertEqual(self.protocol.color, '#C31A00')
566- self.assertEqual(self.protocol.features,
567- ['receive', 'images'])
568- self.assertEqual(self.protocol.default_streams,
569+ self.assertEqual(self.protocol.info.authtype, 'none')
570+ self.assertEqual(self.protocol.info.color, '#C31A00')
571+ self.assertEqual(self.protocol.info.default_streams,
572 ['images'])
573
574 def test_failed_login(self):
575
576=== modified file 'gwibber/gwibber/tests/test_protocols.py'
577--- gwibber/gwibber/tests/test_protocols.py 2012-09-07 19:15:05 +0000
578+++ gwibber/gwibber/tests/test_protocols.py 2012-09-12 21:17:40 +0000
579@@ -44,7 +44,7 @@
580 self.assertNotIn('base', self.manager.protocols)
581 for protocol_class in self.manager.protocols.values():
582 self.assertNotEqual(protocol_class, Base)
583- self.assertIsNotNone(protocol_class.name)
584+ self.assertIsNotNone(protocol_class.__name__)
585
586
587 class FakeAccount(dict):
588@@ -77,24 +77,20 @@
589 # Each protocol carries with it a number of protocol variables. These
590 # are initialized to None in the base class.
591 protocol = MyProtocol(object())
592- self.assertIsNone(protocol.name)
593- self.assertIsNone(protocol.version)
594- self.assertIsNone(protocol.config)
595- self.assertIsNone(protocol.authtype)
596- self.assertIsNone(protocol.color)
597- self.assertIsNone(protocol.features)
598- self.assertIsNone(protocol.default_streams)
599+ self.assertIsNone(protocol.info.version)
600+ self.assertIsNone(protocol.info.config)
601+ self.assertIsNone(protocol.info.authtype)
602+ self.assertIsNone(protocol.info.color)
603+ self.assertIsNone(protocol.info.default_streams)
604
605 def test_no_operation(self):
606 # Trying to call a protocol with a missing operation raises an
607 # AttributeError exception.
608 fake_account = object()
609 my_protocol = MyProtocol(fake_account)
610- with self.assertRaises(AttributeError) as cm:
611+ with self.assertRaises(NotImplementedError) as cm:
612 my_protocol('give_me_a_pony')
613- self.assertEqual(
614- str(cm.exception),
615- "'MyProtocol' object has no attribute 'give_me_a_pony'")
616+ self.assertEqual(str(cm.exception), 'give_me_a_pony')
617
618 def test_private_operation(self):
619 # Trying to call a protocol with a non-public operation raises an
620@@ -106,10 +102,9 @@
621 self.assertEqual(my_protocol.result, 'ant:bee')
622 # But we cannot call the method through the __call__ interface.
623 my_protocol.result = ''
624- with self.assertRaises(AttributeError) as cm:
625+ with self.assertRaises(NotImplementedError) as cm:
626 my_protocol('_private', 'cat', 'dog')
627- self.assertEqual(str(cm.exception),
628- "'MyProtocol' object has no attribute '_private'")
629+ self.assertEqual(str(cm.exception), '_private')
630 self.assertEqual(my_protocol.result, '')
631
632 def test_basic_api_synchronous(self):
633
634=== modified file 'gwibber/gwibber/tests/test_twitter.py'
635--- gwibber/gwibber/tests/test_twitter.py 2012-09-07 19:15:05 +0000
636+++ gwibber/gwibber/tests/test_twitter.py 2012-09-12 21:17:40 +0000
637@@ -51,4 +51,4 @@
638
639 def test_protocol_info(self):
640 # Each protocol carries with it a number of protocol variables.
641- self.assertEqual(self.protocol.name, 'Twitter')
642+ self.assertEqual(self.protocol.__class__.__name__, 'Twitter')
643
644=== modified file 'gwibber/gwibber/utils/account.py'
645--- gwibber/gwibber/utils/account.py 2012-09-07 21:51:08 +0000
646+++ gwibber/gwibber/utils/account.py 2012-09-12 21:17:40 +0000
647@@ -17,6 +17,7 @@
648 __all__ = [
649 'Account',
650 'AccountManager',
651+ 'protocolmanager',
652 ]
653
654
655@@ -31,7 +32,7 @@
656
657
658 log = logging.getLogger('gwibber.accounts')
659-_manager = ProtocolManager()
660+protocolmanager = ProtocolManager()
661
662
663 class AccountManager:
664@@ -80,6 +81,12 @@
665 new_account = Account(account_service)
666 self._accounts[new_account.global_id] = new_account
667
668+ def get_all(self):
669+ return self._accounts.values()
670+
671+ def get(self, account_id, default=None):
672+ return self._accounts.get(account_id, default)
673+
674
675 class Account(dict):
676 """A thin wrapper around libaccounts API."""
677@@ -100,7 +107,7 @@
678 self['id'] = '{}/{}'.format(self.global_id, service_name)
679 # The provider in libaccounts should match the name of our protocol.
680 protocol_name = account.get_provider_name()
681- protocol_class = _manager.protocols.get(protocol_name)
682+ protocol_class = protocolmanager.protocols.get(protocol_name)
683 if protocol_class is None:
684 raise UnsupportedProtocolError(protocol_name)
685 self.protocol = protocol_class(self)
686
687=== modified file 'gwibber/gwibber/utils/protocol.py'
688--- gwibber/gwibber/utils/protocol.py 2012-09-06 16:24:33 +0000
689+++ gwibber/gwibber/utils/protocol.py 2012-09-12 21:17:40 +0000
690@@ -30,14 +30,12 @@
691
692
693 class Base:
694- # Protocol information. All of them default to None.
695- name = None
696- version = None
697- config = None
698- authtype = None
699- color = None
700- features = None
701- default_streams = None
702+ class info:
703+ version = None
704+ config = None
705+ authtype = None
706+ color = None
707+ default_streams = None
708
709 def __init__(self, account):
710 self.account = account
711@@ -49,12 +47,11 @@
712 gwibber-service thread. Note that there is no expectation, and no
713 race-condition safe way, of returning a result from the operation.
714 """
715- # Allow AttributeErrors to propagate up. Treat all non-public
716- # methods, a.k.a. operations, as non-existent.
717- if operation.startswith('_'):
718- raise AttributeError("'{}' object has no attribute '{}'".format(
719- self.__class__.__name__, operation))
720+ if operation.startswith('_') or not hasattr(self, operation):
721+ raise NotImplementedError(operation)
722+
723 method = getattr(self, operation)
724+
725 # Run the operation in a thread, but make sure it's a daemon thread so
726 # that the main thread (i.e. gwibber-service) can exit even if this
727 # thread has not yet completed.
728@@ -102,7 +99,7 @@
729 """Discover all protocol classes."""
730
731 def __init__(self):
732- self.protocols = dict((cls.name.lower(), cls)
733+ self.protocols = dict((cls.__name__.lower(), cls)
734 for cls in self._protocol_classes)
735
736 @property

Subscribers

People subscribed via source and target branches