Merge lp:~robru/gwibber/dispatcher into lp:~barry/gwibber/py3
- dispatcher
- Merge into py3
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Pending | ||
Review via email: mp+123652@code.launchpad.net |
Commit message
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. ;-)
- 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 NotImplementedE
rrors 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
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 |