Merge lp:~gtg-user/gtg/backends-utils into lp:~gtg/gtg/old-trunk
- backends-utils
- Merge into old-trunk
Status: | Merged |
---|---|
Merged at revision: | 880 |
Proposed branch: | lp:~gtg-user/gtg/backends-utils |
Merge into: | lp:~gtg/gtg/old-trunk |
Prerequisite: | lp:~gtg-user/gtg/backends-window |
Diff against target: |
1450 lines (+1385/-0) 12 files modified
CHANGELOG (+1/-0) GTG/backends/periodicimportbackend.py (+117/-0) GTG/backends/syncengine.py (+288/-0) GTG/gtk/browser/custominfobar.py (+211/-0) GTG/tests/test_bidict.py (+79/-0) GTG/tests/test_dates.py (+43/-0) GTG/tests/test_syncengine.py (+189/-0) GTG/tests/test_syncmeme.py (+59/-0) GTG/tests/test_twokeydict.py (+98/-0) GTG/tools/bidict.py (+112/-0) GTG/tools/twokeydict.py (+135/-0) GTG/tools/watchdog.py (+53/-0) |
To merge this branch: | bzr merge lp:~gtg-user/gtg/backends-utils |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gtg developers | Pending | ||
Review via email: mp+32644@code.launchpad.net |
This proposal supersedes a proposal from 2010-08-10.
Commit message
Description of the change
This branch contains all the common utils used by the backends that are not the default one.
Mainly, it contains code for:
- telling if a remote task is new, has to be updated or removed (it's a standalone library)
- getting remote tasks in polling
- a watchdog for stalling functions
The code contained in this merge isn't used by "Trunk" GTG, but it will be as backends are merged: this is why you won't see any difference in GTG's behavior now.
Tests and documentation for these parts is here too.
(lp:~gtg-user/gtg/backends-window should be merged before this one. Some file needed by both are just there to review).
- 881. By Luca Invernizzi
-
disallowing concurrent importing
- 882. By Luca Invernizzi
-
cherrypicking from my development branch
- 883. By Luca Invernizzi
-
more responsiveness in periodic imports
- 884. By Luca Invernizzi
-
merge w/ trunk
Preview Diff
1 | === modified file 'CHANGELOG' |
2 | --- CHANGELOG 2010-08-04 00:30:22 +0000 |
3 | +++ CHANGELOG 2010-08-25 16:29:45 +0000 |
4 | @@ -4,6 +4,7 @@ |
5 | * Fixed bug with data consistency #579189, by Marko Kevac |
6 | * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij |
7 | * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul |
8 | + * Added utilities for complex backends by Luca Invernizzi |
9 | |
10 | 2010-03-01 Getting Things GNOME! 0.2.2 |
11 | * Autostart on login, by Luca Invernizzi |
12 | |
13 | === added file 'GTG/backends/periodicimportbackend.py' |
14 | --- GTG/backends/periodicimportbackend.py 1970-01-01 00:00:00 +0000 |
15 | +++ GTG/backends/periodicimportbackend.py 2010-08-25 16:29:45 +0000 |
16 | @@ -0,0 +1,117 @@ |
17 | +# -*- coding: utf-8 -*- |
18 | +# ----------------------------------------------------------------------------- |
19 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
20 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
21 | +# |
22 | +# This program is free software: you can redistribute it and/or modify it under |
23 | +# the terms of the GNU General Public License as published by the Free Software |
24 | +# Foundation, either version 3 of the License, or (at your option) any later |
25 | +# version. |
26 | +# |
27 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
28 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
29 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
30 | +# details. |
31 | +# |
32 | +# You should have received a copy of the GNU General Public License along with |
33 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
34 | +# ----------------------------------------------------------------------------- |
35 | + |
36 | +''' |
37 | +Contains PeriodicImportBackend, a GenericBackend specialized for checking the |
38 | +remote backend in polling. |
39 | +''' |
40 | + |
41 | +import threading |
42 | + |
43 | +from GTG.backends.genericbackend import GenericBackend |
44 | +from GTG.backends.backendsignals import BackendSignals |
45 | +from GTG.tools.interruptible import interruptible |
46 | + |
47 | + |
48 | + |
49 | +class PeriodicImportBackend(GenericBackend): |
50 | + ''' |
51 | + This class can be used in place of GenericBackend when a periodic import is |
52 | + necessary, as the remote service providing tasks does not signals the |
53 | + changes. |
54 | + To use this, only two things are necessary: |
55 | + - using do_periodic_import instead of start_get_tasks |
56 | + - having in _static_parameters a "period" key, as in |
57 | + "period": { \ |
58 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \ |
59 | + GenericBackend.PARAM_DEFAULT_VALUE: 2, }, |
60 | + This specifies the time that must pass between consecutive imports |
61 | + (in minutes) |
62 | + ''' |
63 | + |
64 | + def __init__(self, parameters): |
65 | + super(PeriodicImportBackend, self).__init__(parameters) |
66 | + self.running_iteration = False |
67 | + self.urgent_iteration = False |
68 | + |
69 | + @interruptible |
70 | + def start_get_tasks(self): |
71 | + ''' |
72 | + This function launches the first periodic import, and schedules the |
73 | + next ones. |
74 | + ''' |
75 | + self.cancellation_point() |
76 | + #if we're already importing, we queue a "urgent" import cycle after this |
77 | + #one. The feeling of responsiveness of the backend is improved. |
78 | + if not self.running_iteration: |
79 | + try: |
80 | + #if an iteration was scheduled, we cancel it |
81 | + if self.import_timer: |
82 | + self.import_timer.cancel() |
83 | + except: |
84 | + pass |
85 | + if self.is_enabled() == False: |
86 | + return |
87 | + |
88 | + #we schedule the next iteration, just in case this one fails |
89 | + if not self.urgent_iteration: |
90 | + self.import_timer = threading.Timer( \ |
91 | + self._parameters['period'] * 60.0, \ |
92 | + self.start_get_tasks) |
93 | + self.import_timer.start() |
94 | + |
95 | + #execute the iteration |
96 | + self.running_iteration = True |
97 | + self._start_get_tasks() |
98 | + self.running_iteration = False |
99 | + self.cancellation_point() |
100 | + |
101 | + #execute eventual urgent iteration |
102 | + #NOTE: this way, if the iteration fails, the whole periodic import |
103 | + # cycle fails. |
104 | + if self.urgent_iteration: |
105 | + self.urgent_iteration = False |
106 | + self.start_get_tasks() |
107 | + else: |
108 | + self.urgent_iteration = True |
109 | + |
110 | + |
111 | + def _start_get_tasks(self): |
112 | + ''' |
113 | + This function executes an imports and schedules the next |
114 | + ''' |
115 | + self.cancellation_point() |
116 | + BackendSignals().backend_sync_started(self.get_id()) |
117 | + self.do_periodic_import() |
118 | + BackendSignals().backend_sync_ended(self.get_id()) |
119 | + |
120 | + def quit(self, disable = False): |
121 | + ''' |
122 | + Called when GTG quits or disconnects the backend. |
123 | + ''' |
124 | + super(PeriodicImportBackend, self).quit(disable) |
125 | + try: |
126 | + self.import_timer.cancel() |
127 | + except Exception: |
128 | + pass |
129 | + try: |
130 | + self.import_timer.join() |
131 | + except Exception: |
132 | + pass |
133 | + |
134 | |
135 | === added file 'GTG/backends/syncengine.py' |
136 | --- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000 |
137 | +++ GTG/backends/syncengine.py 2010-08-25 16:29:45 +0000 |
138 | @@ -0,0 +1,288 @@ |
139 | +# -*- coding: utf-8 -*- |
140 | +# ----------------------------------------------------------------------------- |
141 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
142 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
143 | +# |
144 | +# This program is free software: you can redistribute it and/or modify it under |
145 | +# the terms of the GNU General Public License as published by the Free Software |
146 | +# Foundation, either version 3 of the License, or (at your option) any later |
147 | +# version. |
148 | +# |
149 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
150 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
151 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
152 | +# details. |
153 | +# |
154 | +# You should have received a copy of the GNU General Public License along with |
155 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
156 | +# ----------------------------------------------------------------------------- |
157 | + |
158 | +''' |
159 | +This library deals with synchronizing two sets of objects. |
160 | +It works like this: |
161 | + - We have two sets of generic objects (local and remote) |
162 | + - We present one object of either one of the sets and ask the library what's |
163 | + the state of its synchronization |
164 | + - the library will tell us if we need to add a clone object in the other set, |
165 | + update it or, if the other one has been removed, remove also this one |
166 | +''' |
167 | +from GTG.tools.twokeydict import TwoKeyDict |
168 | + |
169 | + |
170 | +TYPE_LOCAL = "local" |
171 | +TYPE_REMOTE = "remote" |
172 | + |
173 | + |
174 | + |
175 | +class SyncMeme(object): |
176 | + ''' |
177 | + A SyncMeme is the object storing the data needed to keep track of the state |
178 | + of two objects synchronization. |
179 | + This basic version, that can be expanded as needed by the code using the |
180 | + SyncEngine, just stores the modified date and time of the last |
181 | + synchronization for both objects (local and remote) |
182 | + ''' |
183 | + #NOTE: Checking objects CRCs would make this check nicer, as we could know |
184 | + # if the object was really changed, or it has just updated its |
185 | + # modified time (invernizzi) |
186 | + |
187 | + def __init__(self, |
188 | + local_modified = None, |
189 | + remote_modified = None, |
190 | + origin = None): |
191 | + ''' |
192 | + Creates a new SyncMeme, updating the modified times for both the |
193 | + local and remote objects, and sets the given origin. |
194 | + If any of the parameters is set to None, it's ignored. |
195 | + |
196 | + @param local_modified: the modified time for the local object |
197 | + @param remote_modified: the modified time for the remote object |
198 | + @param origin: an object that identifies whether the local or the remote is |
199 | + the original object, the other one being a copy. |
200 | + ''' |
201 | + if local_modified != None: |
202 | + self.set_local_last_modified(local_modified) |
203 | + if remote_modified != None: |
204 | + self.set_remote_last_modified(remote_modified) |
205 | + if origin != None: |
206 | + self.set_origin(origin) |
207 | + |
208 | + def set_local_last_modified(self, modified_datetime): |
209 | + ''' |
210 | + Setter function for the local object modified datetime. |
211 | + |
212 | + @param modified_datetime: the local object modified datetime |
213 | + ''' |
214 | + self.local_last_modified = modified_datetime |
215 | + |
216 | + def get_local_last_modified(self): |
217 | + ''' |
218 | + Getter function for the local object modified datetime. |
219 | + ''' |
220 | + return self.local_last_modified |
221 | + |
222 | + def set_remote_last_modified(self, modified_datetime): |
223 | + ''' |
224 | + Setter function for the remote object modified datetime. |
225 | + |
226 | + @param modified_datetime: the remote object modified datetime |
227 | + ''' |
228 | + self.remote_last_modified = modified_datetime |
229 | + |
230 | + def get_remote_last_modified(self): |
231 | + ''' |
232 | + Getter function for the remote object modified datetime. |
233 | + ''' |
234 | + return self.remote_last_modified |
235 | + |
236 | + def which_is_newest(self, local_modified, remote_modified): |
237 | + ''' |
238 | + Given the updated modified time for both the local and the remote |
239 | + objects, it checks them against the stored modified times and |
240 | + then against each other. |
241 | + |
242 | + @returns string: "local"- if the local object has been modified and its |
243 | + the newest |
244 | + "remote" - the same for the remote object |
245 | + None - if no object modified time is newer than the |
246 | + stored one (the objects have not been modified) |
247 | + ''' |
248 | + if local_modified <= self.local_last_modified and \ |
249 | + remote_modified <= self.remote_last_modified: |
250 | + return None |
251 | + if local_modified > remote_modified: |
252 | + return "local" |
253 | + else: |
254 | + return "remote" |
255 | + |
256 | + def get_origin(self): |
257 | + ''' |
258 | + Returns the name of the source that firstly presented the object |
259 | + ''' |
260 | + return self.origin |
261 | + |
262 | + def set_origin(self, origin): |
263 | + ''' |
264 | + Sets the source that presented the object for the first time. This |
265 | + source holds the original object, while the other holds the copy. |
266 | + This can be useful in the case of "lost syncability" (see the SyncEngine |
267 | + for an explaination). |
268 | + |
269 | + @param origin: object representing the source |
270 | + ''' |
271 | + self.origin = origin |
272 | + |
273 | + |
274 | + |
275 | +class SyncMemes(TwoKeyDict): |
276 | + ''' |
277 | + A TwoKeyDict, with just the names changed to be better understandable. |
278 | + The meaning of these names is explained in the SyncEngine class description. |
279 | + It's used to store a set of SyncMeme objects, each one keeping storing all |
280 | + the data needed to keep track of a single relationship. |
281 | + ''' |
282 | + |
283 | + |
284 | + get_remote_id = TwoKeyDict._get_secondary_key |
285 | + get_local_id = TwoKeyDict._get_primary_key |
286 | + remove_local_id = TwoKeyDict._remove_by_primary |
287 | + remove_remote_id = TwoKeyDict._remove_by_secondary |
288 | + get_meme_from_local_id = TwoKeyDict._get_by_primary |
289 | + get_meme_from_remote_id = TwoKeyDict._get_by_secondary |
290 | + get_all_local = TwoKeyDict._get_all_primary_keys |
291 | + get_all_remote = TwoKeyDict._get_all_secondary_keys |
292 | + |
293 | + |
294 | + |
295 | +class SyncEngine(object): |
296 | + ''' |
297 | + The SyncEngine is an object useful in keeping two sets of objects |
298 | + synchronized. |
299 | + One set is called the Local set, the other is the Remote one. |
300 | + It stores the state of the synchronization and the latest state of each |
301 | + object. |
302 | + When asked, it can tell if a couple of related objects are up to date in the |
303 | + sync and, if not, which one must be updated. |
304 | + |
305 | + It stores the state of each relationship in a series of SyncMeme. |
306 | + ''' |
307 | + |
308 | + |
309 | + UPDATE = "update" |
310 | + REMOVE = "remove" |
311 | + ADD = "add" |
312 | + LOST_SYNCABILITY = "lost syncability" |
313 | + |
314 | + def __init__(self): |
315 | + ''' |
316 | + Initializes the storage of object relationships. |
317 | + ''' |
318 | + self.sync_memes = SyncMemes() |
319 | + |
320 | + def _analyze_element(self, |
321 | + element_id, |
322 | + is_local, |
323 | + has_local, |
324 | + has_remote, |
325 | + is_syncable = True): |
326 | + ''' |
327 | + Given an object that should be synced with another one, |
328 | + it finds out about the related object, and decides whether: |
329 | + - the other object hasn't been created yet (thus must be added) |
330 | + - the other object has been deleted (thus this one must be deleted) |
331 | + - the other object is present, but either one has been changed |
332 | + |
333 | + A particular case happens if the other object is present, but the |
334 | + "is_syncable" parameter (which tells that we intend to keep these two |
335 | + objects in sync) is set to False. In this case, this function returns |
336 | + that the Syncability property has been lost. This case is interesting if |
337 | + we want to delete one of the two objects (the one that has been cloned |
338 | + from the original). |
339 | + |
340 | + @param element_id: the id of the element we're analysing. |
341 | + @param is_local: True if the element analysed is the local one (not the |
342 | + remote) |
343 | + @param has_local: function that accepts an id of the local set and |
344 | + returns True if the element is present |
345 | + @param has_remote: function that accepts an id of the remote set and |
346 | + returns True if the element is present |
347 | + @param is_syncable: explained above |
348 | + @returns string: one of self.UPDATE, self.ADD, self.REMOVE, |
349 | + self.LOST_SYNCABILITY |
350 | + ''' |
351 | + if is_local: |
352 | + get_other_id = self.sync_memes.get_remote_id |
353 | + is_task_present = has_remote |
354 | + else: |
355 | + get_other_id = self.sync_memes.get_local_id |
356 | + is_task_present = has_local |
357 | + |
358 | + try: |
359 | + other_id = get_other_id(element_id) |
360 | + if is_task_present(other_id): |
361 | + if is_syncable: |
362 | + return self.UPDATE, other_id |
363 | + else: |
364 | + return self.LOST_SYNCABILITY, other_id |
365 | + else: |
366 | + return self.REMOVE, None |
367 | + except KeyError: |
368 | + if is_syncable: |
369 | + return self.ADD, None |
370 | + return None, None |
371 | + |
372 | + def analyze_local_id(self, element_id, *other_args): |
373 | + ''' |
374 | + Shortcut to call _analyze_element for a local element |
375 | + ''' |
376 | + return self._analyze_element(element_id, True, *other_args) |
377 | + |
378 | + def analyze_remote_id(self, element_id, *other_args): |
379 | + ''' |
380 | + Shortcut to call _analyze_element for a remote element |
381 | + ''' |
382 | + return self._analyze_element(element_id, False, *other_args) |
383 | + |
384 | + def record_relationship(self, local_id, remote_id, meme): |
385 | + ''' |
386 | + Records that an object from the local set is related with one a remote |
387 | + set. |
388 | + |
389 | + @param local_id: the id of the local task |
390 | + @param remote_id: the id of the remote task |
391 | + @param meme: the SyncMeme that keeps track of the relationship |
392 | + ''' |
393 | + triplet = (local_id, remote_id, meme) |
394 | + self.sync_memes.add(triplet) |
395 | + |
396 | + def break_relationship(self, local_id = None, remote_id = None): |
397 | + ''' |
398 | + breaks a relationship between two objects. |
399 | + Only one of the two parameters is necessary to identify the |
400 | + relationship. |
401 | + |
402 | + @param local_id: the id of the local task |
403 | + @param remote_id: the id of the remote task |
404 | + ''' |
405 | + if local_id: |
406 | + self.sync_memes.remove_local_id(local_id) |
407 | + elif remote_id: |
408 | + self.sync_memes.remove_remote_id(remote_id) |
409 | + |
410 | + def __getattr__(self, attr): |
411 | + ''' |
412 | + The functions listed here are passed directly to the SyncMeme object |
413 | + |
414 | + @param attr: a function name among the ones listed here |
415 | + @returns object: the function return object. |
416 | + ''' |
417 | + if attr in ['get_remote_id', |
418 | + 'get_local_id', |
419 | + 'get_meme_from_local_id', |
420 | + 'get_meme_from_remote_id', |
421 | + 'get_all_local', |
422 | + 'get_all_remote']: |
423 | + return getattr(self.sync_memes, attr) |
424 | + else: |
425 | + raise AttributeError |
426 | + |
427 | |
428 | === added file 'GTG/gtk/browser/custominfobar.py' |
429 | --- GTG/gtk/browser/custominfobar.py 1970-01-01 00:00:00 +0000 |
430 | +++ GTG/gtk/browser/custominfobar.py 2010-08-25 16:29:45 +0000 |
431 | @@ -0,0 +1,211 @@ |
432 | +# -*- coding: utf-8 -*- |
433 | +# ----------------------------------------------------------------------------- |
434 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
435 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
436 | +# |
437 | +# This program is free software: you can redistribute it and/or modify it under |
438 | +# the terms of the GNU General Public License as published by the Free Software |
439 | +# Foundation, either version 3 of the License, or (at your option) any later |
440 | +# version. |
441 | +# |
442 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
443 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
444 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
445 | +# details. |
446 | +# |
447 | +# You should have received a copy of the GNU General Public License along with |
448 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
449 | +# ----------------------------------------------------------------------------- |
450 | + |
451 | +import gtk |
452 | +import threading |
453 | + |
454 | +from GTG import _ |
455 | +from GTG.backends.backendsignals import BackendSignals |
456 | +from GTG.tools.networkmanager import is_connection_up |
457 | + |
458 | + |
459 | + |
460 | +class CustomInfoBar(gtk.InfoBar): |
461 | + ''' |
462 | + A gtk.InfoBar specialized for displaying errors and requests for |
463 | + interaction coming from the backends |
464 | + ''' |
465 | + |
466 | + |
467 | + AUTHENTICATION_MESSAGE = _("The <b>%s</b> backend cannot login with the " |
468 | + "supplied authentication data and has been" |
469 | + " disabled. To retry the login, re-enable the backend.") |
470 | + |
471 | + NETWORK_MESSAGE = _("Due to a network problem, I cannot contact " |
472 | + "the <b>%s</b> backend.") |
473 | + |
474 | + DBUS_MESSAGE = _("Cannot connect to DBUS, I've disabled " |
475 | + "the <b>%s</b> backend.") |
476 | + |
477 | + def __init__(self, req, browser, vmanager, backend_id): |
478 | + ''' |
479 | + Constructor, Prepares the infobar. |
480 | + |
481 | + @param req: a Requester object |
482 | + @param browser: a TaskBrowser object |
483 | + @param vmanager: a ViewManager object |
484 | + @param backend_id: the id of the backend linked to the infobar |
485 | + ''' |
486 | + super(CustomInfoBar, self).__init__() |
487 | + self.req = req |
488 | + self.browser = browser |
489 | + self.vmanager = vmanager |
490 | + self.backend_id = backend_id |
491 | + self.backend = self.req.get_backend(backend_id) |
492 | + |
493 | + def get_backend_id(self): |
494 | + ''' |
495 | + Getter function to return the id of the backend for which this |
496 | + gtk.InfoBar was created |
497 | + ''' |
498 | + return self.backend_id |
499 | + |
500 | + def _populate(self): |
501 | + '''Setting up gtk widgets''' |
502 | + content_hbox = self.get_content_area() |
503 | + content_hbox.set_homogeneous(False) |
504 | + self.label = gtk.Label() |
505 | + self.label.set_line_wrap(True) |
506 | + self.label.set_alignment(0.5, 0.5) |
507 | + self.label.set_justify(gtk.JUSTIFY_FILL) |
508 | + content_hbox.pack_start(self.label, True, True) |
509 | + |
510 | + def _on_error_response(self, widget, event): |
511 | + ''' |
512 | + Signal callback executed when the user acknowledges the error displayed |
513 | + in the infobar |
514 | + |
515 | + @param widget: not used, here for compatibility with signals callbacks |
516 | + @param event: the code of the gtk response |
517 | + ''' |
518 | + self.hide() |
519 | + if event == gtk.RESPONSE_ACCEPT: |
520 | + self.vmanager.configure_backend(backend_id = self.backend_id) |
521 | + |
522 | + def set_error_code(self, error_code): |
523 | + ''' |
524 | + Sets this infobar to show an error to the user |
525 | + |
526 | + @param error_code: the code of the error to show. Error codes are listed |
527 | + in BackendSignals |
528 | + ''' |
529 | + self._populate() |
530 | + self.connect("response", self._on_error_response) |
531 | + backend_name = self.backend.get_human_name() |
532 | + |
533 | + if error_code == BackendSignals.ERRNO_AUTHENTICATION: |
534 | + self.set_message_type(gtk.MESSAGE_ERROR) |
535 | + self.label.set_markup(self.AUTHENTICATION_MESSAGE % backend_name) |
536 | + self.add_button(_('Configure backend'), gtk.RESPONSE_ACCEPT) |
537 | + self.add_button(_('Ignore'), gtk.RESPONSE_CLOSE) |
538 | + |
539 | + elif error_code == BackendSignals.ERRNO_NETWORK: |
540 | + if not is_connection_up(): |
541 | + return |
542 | + self.set_message_type(gtk.MESSAGE_WARNING) |
543 | + self.label.set_markup(self.NETWORK_MESSAGE % backend_name) |
544 | + #FIXME: use gtk stock button instead |
545 | + self.add_button(_('Ok'), gtk.RESPONSE_CLOSE) |
546 | + |
547 | + elif error_code == BackendSignals.ERRNO_DBUS: |
548 | + self.set_message_type(gtk.MESSAGE_WARNING) |
549 | + self.label.set_markup(self.DBUS_MESSAGE % backend_name) |
550 | + self.add_button(_('Ok'), gtk.RESPONSE_CLOSE) |
551 | + |
552 | + self.show_all() |
553 | + |
554 | + def set_interaction_request(self, description, interaction_type, callback): |
555 | + ''' |
556 | + Sets this infobar to request an interaction from the user |
557 | + |
558 | + @param description: a string describing the interaction needed |
559 | + @param interaction_type: a string describing the type of interaction |
560 | + (yes/no, only confirm, ok/cancel...) |
561 | + @param callback: the function to call when the user provides the |
562 | + feedback |
563 | + ''' |
564 | + self._populate() |
565 | + self.callback = callback |
566 | + self.set_message_type(gtk.MESSAGE_INFO) |
567 | + self.label.set_markup(description) |
568 | + self.connect("response", self._on_interaction_response) |
569 | + self.interaction_type = interaction_type |
570 | + if interaction_type == BackendSignals().INTERACTION_CONFIRM: |
571 | + self.add_button(_('Confirm'), gtk.RESPONSE_ACCEPT) |
572 | + elif interaction_type == BackendSignals().INTERACTION_TEXT: |
573 | + self.add_button(_('Continue'), gtk.RESPONSE_ACCEPT) |
574 | + self.show_all() |
575 | + |
576 | + def _on_interaction_response(self, widget, event): |
577 | + ''' |
578 | + Signal callback executed when the user gives the feedback for a |
579 | + requested interaction |
580 | + |
581 | + @param widget: not used, here for compatibility with signals callbacks |
582 | + @param event: the code of the gtk response |
583 | + ''' |
584 | + if event == gtk.RESPONSE_ACCEPT: |
585 | + if self.interaction_type == BackendSignals().INTERACTION_TEXT: |
586 | + self._prepare_textual_interaction() |
587 | + print "done" |
588 | + elif self.interaction_type == BackendSignals().INTERACTION_CONFIRM: |
589 | + self.hide() |
590 | + threading.Thread(target = getattr(self.backend, |
591 | + self.callback)).start() |
592 | + |
593 | + def _prepare_textual_interaction(self): |
594 | + ''' |
595 | + Helper function. gtk calls to populate the infobar in the case of |
596 | + interaction request |
597 | + ''' |
598 | + title, description\ |
599 | + = getattr(self.backend, |
600 | + self.callback)("get_ui_dialog_text") |
601 | + self.dialog = gtk.Window()#type = gtk.WINDOW_POPUP) |
602 | + self.dialog.set_title(title) |
603 | + self.dialog.set_transient_for(self.browser.window) |
604 | + self.dialog.set_destroy_with_parent(True) |
605 | + self.dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT) |
606 | + self.dialog.set_modal(True) |
607 | + # self.dialog.set_size_request(300,170) |
608 | + vbox = gtk.VBox() |
609 | + self.dialog.add(vbox) |
610 | + description_label = gtk.Label() |
611 | + description_label.set_justify(gtk.JUSTIFY_FILL) |
612 | + description_label.set_line_wrap(True) |
613 | + description_label.set_markup(description) |
614 | + align = gtk.Alignment(0.5, 0.5, 1, 1) |
615 | + align.set_padding(10, 0, 20, 20) |
616 | + align.add(description_label) |
617 | + vbox.pack_start(align) |
618 | + self.text_box = gtk.Entry() |
619 | + self.text_box.set_size_request(-1, 40) |
620 | + align = gtk.Alignment(0.5, 0.5, 1, 1) |
621 | + align.set_padding(20, 20, 20, 20) |
622 | + align.add(self.text_box) |
623 | + vbox.pack_start(align) |
624 | + button = gtk.Button(stock = gtk.STOCK_OK) |
625 | + button.connect("clicked", self._on_text_confirmed) |
626 | + button.set_size_request(-1, 40) |
627 | + vbox.pack_start(button, False) |
628 | + self.dialog.show_all() |
629 | + self.hide() |
630 | + |
631 | + def _on_text_confirmed(self, widget): |
632 | + ''' |
633 | + Signal callback, used when the interaction needs a textual input to be |
634 | + completed (e.g, the twitter OAuth, requesting a pin) |
635 | + |
636 | + @param widget: not used, here for signal callback compatibility |
637 | + ''' |
638 | + text = self.text_box.get_text() |
639 | + self.dialog.destroy() |
640 | + threading.Thread(target = getattr(self.backend, self.callback), |
641 | + args = ("set_text", text)).start() |
642 | + |
643 | |
644 | === added file 'GTG/tests/test_bidict.py' |
645 | --- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000 |
646 | +++ GTG/tests/test_bidict.py 2010-08-25 16:29:45 +0000 |
647 | @@ -0,0 +1,79 @@ |
648 | +# -*- coding: utf-8 -*- |
649 | +# ----------------------------------------------------------------------------- |
650 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
651 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
652 | +# |
653 | +# This program is free software: you can redistribute it and/or modify it under |
654 | +# the terms of the GNU General Public License as published by the Free Software |
655 | +# Foundation, either version 3 of the License, or (at your option) any later |
656 | +# version. |
657 | +# |
658 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
659 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
660 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
661 | +# details. |
662 | +# |
663 | +# You should have received a copy of the GNU General Public License along with |
664 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
665 | +# ----------------------------------------------------------------------------- |
666 | + |
667 | +''' |
668 | +Tests for the diDict class |
669 | +''' |
670 | + |
671 | +import unittest |
672 | +import uuid |
673 | + |
674 | +from GTG.tools.bidict import BiDict |
675 | + |
676 | + |
677 | + |
678 | +class TestBiDict(unittest.TestCase): |
679 | + ''' |
680 | + Tests for the BiDict object. |
681 | + ''' |
682 | + |
683 | + |
684 | + def test_add_and_gets(self): |
685 | + ''' |
686 | + Test for the __init__, _get_by_first, _get_by_second function |
687 | + ''' |
688 | + pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)] |
689 | + bidict = BiDict(*pairs) |
690 | + for pair in pairs: |
691 | + self.assertEqual(bidict._get_by_first(pair[0]), pair[1]) |
692 | + self.assertEqual(bidict._get_by_second(pair[1]), pair[0]) |
693 | + |
694 | + def test_remove_by_first_or_second(self): |
695 | + ''' |
696 | + Tests for removing elements from the biDict |
697 | + ''' |
698 | + pair_first = (1, 'one') |
699 | + pair_second = (2, 'two') |
700 | + bidict = BiDict(pair_first, pair_second) |
701 | + bidict._remove_by_first(pair_first[0]) |
702 | + bidict._remove_by_second(pair_second[1]) |
703 | + missing_first = 0 |
704 | + missing_second = 0 |
705 | + try: |
706 | + bidict._get_by_first(pair_first[0]) |
707 | + except KeyError: |
708 | + missing_first += 1 |
709 | + try: |
710 | + bidict._get_by_first(pair_second[0]) |
711 | + except KeyError: |
712 | + missing_first += 1 |
713 | + try: |
714 | + bidict._get_by_second(pair_first[1]) |
715 | + except KeyError: |
716 | + missing_second += 1 |
717 | + try: |
718 | + bidict._get_by_second(pair_second[1]) |
719 | + except KeyError: |
720 | + missing_second += 1 |
721 | + self.assertEqual(missing_first, 2) |
722 | + self.assertEqual(missing_second, 2) |
723 | + |
724 | +def test_suite(): |
725 | + return unittest.TestLoader().loadTestsFromTestCase(TestBiDict) |
726 | + |
727 | |
728 | === added file 'GTG/tests/test_dates.py' |
729 | --- GTG/tests/test_dates.py 1970-01-01 00:00:00 +0000 |
730 | +++ GTG/tests/test_dates.py 2010-08-25 16:29:45 +0000 |
731 | @@ -0,0 +1,43 @@ |
732 | +# -*- coding: utf-8 -*- |
733 | +# ----------------------------------------------------------------------------- |
734 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
735 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
736 | +# |
737 | +# This program is free software: you can redistribute it and/or modify it under |
738 | +# the terms of the GNU General Public License as published by the Free Software |
739 | +# Foundation, either version 3 of the License, or (at your option) any later |
740 | +# version. |
741 | +# |
742 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
743 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
744 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
745 | +# details. |
746 | +# |
747 | +# You should have received a copy of the GNU General Public License along with |
748 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
749 | +# ----------------------------------------------------------------------------- |
750 | + |
751 | +''' |
752 | +Tests for the various Date classes |
753 | +''' |
754 | + |
755 | +import unittest |
756 | + |
757 | +from GTG.tools.dates import get_canonical_date |
758 | + |
759 | +class TestDates(unittest.TestCase): |
760 | + ''' |
761 | + Tests for the various Date classes |
762 | + ''' |
763 | + |
764 | + def test_get_canonical_date(self): |
765 | + ''' |
766 | + Tests for "get_canonical_date" |
767 | + ''' |
768 | + for str in ["1985-03-29", "now", "soon", "later", ""]: |
769 | + date = get_canonical_date(str) |
770 | + self.assertEqual(date.__str__(), str) |
771 | + |
772 | +def test_suite(): |
773 | + return unittest.TestLoader().loadTestsFromTestCase(TestDates) |
774 | + |
775 | |
776 | === added file 'GTG/tests/test_syncengine.py' |
777 | --- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000 |
778 | +++ GTG/tests/test_syncengine.py 2010-08-25 16:29:45 +0000 |
779 | @@ -0,0 +1,189 @@ |
780 | +# -*- coding: utf-8 -*- |
781 | +# ----------------------------------------------------------------------------- |
782 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
783 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
784 | +# |
785 | +# This program is free software: you can redistribute it and/or modify it under |
786 | +# the terms of the GNU General Public License as published by the Free Software |
787 | +# Foundation, either version 3 of the License, or (at your option) any later |
788 | +# version. |
789 | +# |
790 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
791 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
792 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
793 | +# details. |
794 | +# |
795 | +# You should have received a copy of the GNU General Public License along with |
796 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
797 | +# ----------------------------------------------------------------------------- |
798 | + |
799 | +''' |
800 | +Tests for the SyncEngine class |
801 | +''' |
802 | + |
803 | +import unittest |
804 | +import uuid |
805 | + |
806 | +from GTG.backends.syncengine import SyncEngine |
807 | + |
808 | + |
809 | + |
810 | +class TestSyncEngine(unittest.TestCase): |
811 | + ''' |
812 | + Tests for the SyncEngine object. |
813 | + ''' |
814 | + |
815 | + def setUp(self): |
816 | + self.ftp_local = FakeTaskProvider() |
817 | + self.ftp_remote = FakeTaskProvider() |
818 | + self.sync_engine = SyncEngine() |
819 | + |
820 | + def test_analyze_element_and_record_and_break_relationship(self): |
821 | + ''' |
822 | + Test for the _analyze_element, analyze_remote_id, analyze_local_id, |
823 | + record_relationship, break_relationship |
824 | + ''' |
825 | + #adding a new local task |
826 | + local_id = uuid.uuid4() |
827 | + self.ftp_local.fake_add_task(local_id) |
828 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
829 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
830 | + (SyncEngine.ADD, None)) |
831 | + #creating the related remote task |
832 | + remote_id = uuid.uuid4() |
833 | + self.ftp_remote.fake_add_task(remote_id) |
834 | + #informing the sync_engine about that |
835 | + self.sync_engine.record_relationship(local_id, remote_id, object()) |
836 | + #verifying that it understood that |
837 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
838 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
839 | + (SyncEngine.UPDATE, remote_id)) |
840 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
841 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
842 | + (SyncEngine.UPDATE, local_id)) |
843 | + #and not the reverse |
844 | + self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \ |
845 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
846 | + (SyncEngine.ADD, None)) |
847 | + self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \ |
848 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
849 | + (SyncEngine.ADD, None)) |
850 | + #now we remove the remote task |
851 | + self.ftp_remote.fake_remove_task(remote_id) |
852 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
853 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
854 | + (SyncEngine.REMOVE, None)) |
855 | + self.sync_engine.break_relationship(local_id = local_id) |
856 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
857 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
858 | + (SyncEngine.ADD, None)) |
859 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
860 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
861 | + (SyncEngine.ADD, None)) |
862 | + #we add them back and remove giving the remote id as key to find what to |
863 | + #delete |
864 | + self.ftp_local.fake_add_task(local_id) |
865 | + self.ftp_remote.fake_add_task(remote_id) |
866 | + self.ftp_remote.fake_remove_task(remote_id) |
867 | + self.sync_engine.record_relationship(local_id, remote_id, object) |
868 | + self.sync_engine.break_relationship(remote_id = remote_id) |
869 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
870 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
871 | + (SyncEngine.ADD, None)) |
872 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
873 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
874 | + (SyncEngine.ADD, None)) |
875 | + |
876 | + def test_syncability(self): |
877 | + ''' |
878 | + Test for the _analyze_element, analyze_remote_id, analyze_local_id. |
879 | + Checks that the is_syncable parameter is used correctly |
880 | + ''' |
881 | + #adding a new local task unsyncable |
882 | + local_id = uuid.uuid4() |
883 | + self.ftp_local.fake_add_task(local_id) |
884 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
885 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
886 | + False), \ |
887 | + (None, None)) |
888 | + #adding a new local task, syncable |
889 | + local_id = uuid.uuid4() |
890 | + self.ftp_local.fake_add_task(local_id) |
891 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
892 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
893 | + (SyncEngine.ADD, None)) |
894 | + #creating the related remote task |
895 | + remote_id = uuid.uuid4() |
896 | + self.ftp_remote.fake_add_task(remote_id) |
897 | + #informing the sync_engine about that |
898 | + self.sync_engine.record_relationship(local_id, remote_id, object()) |
899 | + #checking that it behaves correctly with established relationships |
900 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
901 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
902 | + True), \ |
903 | + (SyncEngine.UPDATE, remote_id)) |
904 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
905 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
906 | + False), \ |
907 | + (SyncEngine.LOST_SYNCABILITY, remote_id)) |
908 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
909 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
910 | + True), \ |
911 | + (SyncEngine.UPDATE, local_id)) |
912 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
913 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
914 | + False), \ |
915 | + (SyncEngine.LOST_SYNCABILITY, local_id)) |
916 | + #now we remove the remote task |
917 | + self.ftp_remote.fake_remove_task(remote_id) |
918 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
919 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
920 | + True), \ |
921 | + (SyncEngine.REMOVE, None)) |
922 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
923 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
924 | + False), \ |
925 | + (SyncEngine.REMOVE, None)) |
926 | + self.sync_engine.break_relationship(local_id = local_id) |
927 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
928 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
929 | + True), \ |
930 | + (SyncEngine.ADD, None)) |
931 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
932 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
933 | + False), \ |
934 | + (None, None)) |
935 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
936 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
937 | + True), \ |
938 | + (SyncEngine.ADD, None)) |
939 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
940 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
941 | + False), \ |
942 | + (None, None)) |
943 | + |
944 | +def test_suite(): |
945 | + return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine) |
946 | + |
947 | + |
948 | +class FakeTaskProvider(object): |
949 | + |
950 | + def __init__(self): |
951 | + self.dic = {} |
952 | + |
953 | + def has_task(self, tid): |
954 | + return self.dic.has_key(tid) |
955 | + |
956 | +############################################################################### |
957 | +### Function with the fake_ prefix are here to assist in testing, they do not |
958 | +### need to be present in the real class |
959 | +############################################################################### |
960 | + |
961 | + def fake_add_task(self, tid): |
962 | + self.dic[tid] = "something" |
963 | + |
964 | + def fake_get_task(self, tid): |
965 | + return self.dic[tid] |
966 | + |
967 | + def fake_remove_task(self, tid): |
968 | + del self.dic[tid] |
969 | |
970 | === added file 'GTG/tests/test_syncmeme.py' |
971 | --- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000 |
972 | +++ GTG/tests/test_syncmeme.py 2010-08-25 16:29:45 +0000 |
973 | @@ -0,0 +1,59 @@ |
974 | +# -*- coding: utf-8 -*- |
975 | +# ----------------------------------------------------------------------------- |
976 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
977 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
978 | +# |
979 | +# This program is free software: you can redistribute it and/or modify it under |
980 | +# the terms of the GNU General Public License as published by the Free Software |
981 | +# Foundation, either version 3 of the License, or (at your option) any later |
982 | +# version. |
983 | +# |
984 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
985 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
986 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
987 | +# details. |
988 | +# |
989 | +# You should have received a copy of the GNU General Public License along with |
990 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
991 | +# ----------------------------------------------------------------------------- |
992 | + |
993 | +''' |
994 | +Tests for the SyncMeme class |
995 | +''' |
996 | + |
997 | +import unittest |
998 | +import datetime |
999 | + |
1000 | +from GTG.backends.syncengine import SyncMeme |
1001 | + |
1002 | + |
1003 | + |
1004 | +class TestSyncMeme(unittest.TestCase): |
1005 | + ''' |
1006 | + Tests for the SyncEngine object. |
1007 | + ''' |
1008 | + |
1009 | + def test_which_is_newest(self): |
1010 | + ''' |
1011 | + test the which_is_newest function |
1012 | + |
1013 | + ''' |
1014 | + meme = SyncMeme() |
1015 | + #tasks have not changed |
1016 | + local_modified = datetime.datetime.now() |
1017 | + remote_modified = datetime.datetime.now() |
1018 | + meme.set_local_last_modified(local_modified) |
1019 | + meme.set_remote_last_modified(remote_modified) |
1020 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
1021 | + remote_modified), None) |
1022 | + #we update the local |
1023 | + local_modified = datetime.datetime.now() |
1024 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
1025 | + remote_modified), 'local') |
1026 | + #we update the remote |
1027 | + remote_modified = datetime.datetime.now() |
1028 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
1029 | + remote_modified), 'remote') |
1030 | +def test_suite(): |
1031 | + return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme) |
1032 | + |
1033 | |
1034 | === added file 'GTG/tests/test_twokeydict.py' |
1035 | --- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000 |
1036 | +++ GTG/tests/test_twokeydict.py 2010-08-25 16:29:45 +0000 |
1037 | @@ -0,0 +1,98 @@ |
1038 | +# -*- coding: utf-8 -*- |
1039 | +# ----------------------------------------------------------------------------- |
1040 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
1041 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1042 | +# |
1043 | +# This program is free software: you can redistribute it and/or modify it under |
1044 | +# the terms of the GNU General Public License as published by the Free Software |
1045 | +# Foundation, either version 3 of the License, or (at your option) any later |
1046 | +# version. |
1047 | +# |
1048 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1049 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1050 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1051 | +# details. |
1052 | +# |
1053 | +# You should have received a copy of the GNU General Public License along with |
1054 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1055 | +# ----------------------------------------------------------------------------- |
1056 | + |
1057 | +''' |
1058 | +Tests for the TwoKeyDict class |
1059 | +''' |
1060 | + |
1061 | +import unittest |
1062 | +import uuid |
1063 | + |
1064 | +from GTG.tools.twokeydict import TwoKeyDict |
1065 | + |
1066 | + |
1067 | + |
1068 | +class TestTwoKeyDict(unittest.TestCase): |
1069 | + ''' |
1070 | + Tests for the TwoKeyDict object. |
1071 | + ''' |
1072 | + |
1073 | + |
1074 | + def test_add_and_gets(self): |
1075 | + ''' |
1076 | + Test for the __init__, _get_by_first, _get_by_second function |
1077 | + ''' |
1078 | + triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \ |
1079 | + for a in xrange(10)] |
1080 | + tw_dict = TwoKeyDict(*triplets) |
1081 | + for triplet in triplets: |
1082 | + self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2]) |
1083 | + self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2]) |
1084 | + |
1085 | + def test_remove_by_first_or_second(self): |
1086 | + ''' |
1087 | + Test for removing triplets form the TwoKeyDict |
1088 | + ''' |
1089 | + triplet_first = (1, 'I', 'one') |
1090 | + triplet_second = (2, 'II', 'two') |
1091 | + tw_dict = TwoKeyDict(triplet_first, triplet_second) |
1092 | + tw_dict._remove_by_primary(triplet_first[0]) |
1093 | + tw_dict._remove_by_secondary(triplet_second[1]) |
1094 | + missing_first = 0 |
1095 | + missing_second = 0 |
1096 | + try: |
1097 | + tw_dict._get_by_primary(triplet_first[0]) |
1098 | + except KeyError: |
1099 | + missing_first += 1 |
1100 | + try: |
1101 | + tw_dict._get_by_secondary(triplet_second[0]) |
1102 | + except KeyError: |
1103 | + missing_first += 1 |
1104 | + try: |
1105 | + tw_dict._get_by_secondary(triplet_first[1]) |
1106 | + except KeyError: |
1107 | + missing_second += 1 |
1108 | + try: |
1109 | + tw_dict._get_by_secondary(triplet_second[1]) |
1110 | + except KeyError: |
1111 | + missing_second += 1 |
1112 | + self.assertEqual(missing_first, 2) |
1113 | + self.assertEqual(missing_second, 2) |
1114 | + #check for memory leaks |
1115 | + dict_len = 0 |
1116 | + for key in tw_dict._primary_to_value.iterkeys(): |
1117 | + dict_len += 1 |
1118 | + self.assertEqual(dict_len, 0) |
1119 | + |
1120 | + def test_get_primary_and_secondary_key(self): |
1121 | + ''' |
1122 | + Test for fetching the objects stored in the TwoKeyDict |
1123 | + ''' |
1124 | + triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \ |
1125 | + for a in xrange(10)] |
1126 | + tw_dict = TwoKeyDict(*triplets) |
1127 | + for triplet in triplets: |
1128 | + self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \ |
1129 | + triplet[1]) |
1130 | + self.assertEqual(tw_dict._get_primary_key(triplet[1]), \ |
1131 | + triplet[0]) |
1132 | + |
1133 | +def test_suite(): |
1134 | + return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict) |
1135 | + |
1136 | |
1137 | === added file 'GTG/tools/bidict.py' |
1138 | --- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000 |
1139 | +++ GTG/tools/bidict.py 2010-08-25 16:29:45 +0000 |
1140 | @@ -0,0 +1,112 @@ |
1141 | +# -*- coding: utf-8 -*- |
1142 | +# ----------------------------------------------------------------------------- |
1143 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
1144 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1145 | +# |
1146 | +# This program is free software: you can redistribute it and/or modify it under |
1147 | +# the terms of the GNU General Public License as published by the Free Software |
1148 | +# Foundation, either version 3 of the License, or (at your option) any later |
1149 | +# version. |
1150 | +# |
1151 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1152 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1153 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1154 | +# details. |
1155 | +# |
1156 | +# You should have received a copy of the GNU General Public License along with |
1157 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1158 | +# ----------------------------------------------------------------------------- |
1159 | + |
1160 | + |
1161 | + |
1162 | +class BiDict(object): |
1163 | + ''' |
1164 | + Bidirectional dictionary: the pairs stored can be accessed using either the |
1165 | + first or the second element as key (named key1 and key2). |
1166 | + You don't need this if there is no clash between the domains of the first |
1167 | + and second element of the pairs. |
1168 | + ''' |
1169 | + |
1170 | + def __init__(self, *pairs): |
1171 | + ''' |
1172 | + Initialization of the bidirectional dictionary |
1173 | + |
1174 | + @param pairs: optional. A list of pairs to add to the dictionary |
1175 | + ''' |
1176 | + super(BiDict, self).__init__() |
1177 | + self._first_to_second = {} |
1178 | + self._second_to_first = {} |
1179 | + for pair in pairs: |
1180 | + self.add(pair) |
1181 | + |
1182 | + def add(self, pair): |
1183 | + ''' |
1184 | + Adds a pair (key1, key2) to the dictionary |
1185 | + |
1186 | + @param pair: the pair formatted as (key1, key2) |
1187 | + ''' |
1188 | + self._first_to_second[pair[0]] = pair[1] |
1189 | + self._second_to_first[pair[1]] = pair[0] |
1190 | + |
1191 | + def _get_by_first(self, key): |
1192 | + ''' |
1193 | + Gets the key2 given key1 |
1194 | + |
1195 | + @param key: the first key |
1196 | + ''' |
1197 | + return self._first_to_second[key] |
1198 | + |
1199 | + def _get_by_second(self, key): |
1200 | + ''' |
1201 | + Gets the key1 given key2 |
1202 | + |
1203 | + @param key: the second key |
1204 | + ''' |
1205 | + return self._second_to_first[key] |
1206 | + |
1207 | + def _remove_by_first(self, first): |
1208 | + ''' |
1209 | + Removes a pair given the first key |
1210 | + |
1211 | + @param key: the first key |
1212 | + ''' |
1213 | + second = self._first_to_second[first] |
1214 | + del self._second_to_first[second] |
1215 | + del self._first_to_second[first] |
1216 | + |
1217 | + def _remove_by_second(self, second): |
1218 | + ''' |
1219 | + Removes a pair given the second key |
1220 | + |
1221 | + @param key: the second key |
1222 | + ''' |
1223 | + first = self._second_to_first[second] |
1224 | + del self._first_to_second[first] |
1225 | + del self._second_to_first[second] |
1226 | + |
1227 | + def _get_all_first(self): |
1228 | + ''' |
1229 | + Returns the list of all first keys |
1230 | + |
1231 | + @returns list |
1232 | + ''' |
1233 | + return list(self._first_to_second) |
1234 | + |
1235 | + def _get_all_second(self): |
1236 | + ''' |
1237 | + Returns the list of all second keys |
1238 | + |
1239 | + @returns list |
1240 | + ''' |
1241 | + return list(self._second_to_first) |
1242 | + |
1243 | + def __str__(self): |
1244 | + ''' |
1245 | + returns a string representing the content of this BiDict |
1246 | + |
1247 | + @returns string |
1248 | + ''' |
1249 | + return reduce(lambda text, keys: \ |
1250 | + str(text) + str(keys), |
1251 | + self._first_to_second.iteritems()) |
1252 | + |
1253 | |
1254 | === added file 'GTG/tools/twokeydict.py' |
1255 | --- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000 |
1256 | +++ GTG/tools/twokeydict.py 2010-08-25 16:29:45 +0000 |
1257 | @@ -0,0 +1,135 @@ |
1258 | +# -*- coding: utf-8 -*- |
1259 | +# ----------------------------------------------------------------------------- |
1260 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
1261 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1262 | +# |
1263 | +# This program is free software: you can redistribute it and/or modify it under |
1264 | +# the terms of the GNU General Public License as published by the Free Software |
1265 | +# Foundation, either version 3 of the License, or (at your option) any later |
1266 | +# version. |
1267 | +# |
1268 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1269 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1270 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1271 | +# details. |
1272 | +# |
1273 | +# You should have received a copy of the GNU General Public License along with |
1274 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1275 | +# ----------------------------------------------------------------------------- |
1276 | + |
1277 | +''' |
1278 | +Contains TwoKeyDict, a Dictionary which also has a secondary key |
1279 | +''' |
1280 | + |
1281 | +from GTG.tools.bidict import BiDict |
1282 | + |
1283 | + |
1284 | + |
1285 | +class TwoKeyDict(object): |
1286 | + ''' |
1287 | + It's a standard Dictionary with a secondary key. |
1288 | + For example, you can add an element ('2', 'II', two'), where the |
1289 | + first two arguments are keys and the third is the stored object, and access |
1290 | + it as: |
1291 | + twokey['2'] ==> 'two' |
1292 | + twokey['II'] ==> 'two' |
1293 | + You can also request the other key, given one. |
1294 | + Function calls start with _ because you'll probably want to rename them when |
1295 | + you use this dictionary, for the sake of clarity. |
1296 | + ''' |
1297 | + |
1298 | + |
1299 | + def __init__(self, *triplets): |
1300 | + ''' |
1301 | + Creates the TwoKeyDict and optionally populates it with some data |
1302 | + |
1303 | + @oaram triplets: tuples for populating the TwoKeyDict. Format: |
1304 | + ((key1, key2, data_to_store), ...) |
1305 | + ''' |
1306 | + super(TwoKeyDict, self).__init__() |
1307 | + self._key_to_key_bidict = BiDict() |
1308 | + self._primary_to_value = {} |
1309 | + for triplet in triplets: |
1310 | + self.add(triplet) |
1311 | + |
1312 | + def add(self, triplet): |
1313 | + ''' |
1314 | + Adds a new triplet to the TwoKeyDict |
1315 | + |
1316 | + @param triplet: a tuple formatted like this: |
1317 | + (key1, key2, data_to_store) |
1318 | + ''' |
1319 | + self._key_to_key_bidict.add((triplet[0], triplet[1])) |
1320 | + self._primary_to_value[triplet[0]] = triplet[2] |
1321 | + |
1322 | + def _get_by_primary(self, primary): |
1323 | + ''' |
1324 | + Gets the stored data given the primary key |
1325 | + |
1326 | + @param primary: the primary key |
1327 | + @returns object: the stored object |
1328 | + ''' |
1329 | + return self._primary_to_value[primary] |
1330 | + |
1331 | + def _get_by_secondary(self, secondary): |
1332 | + ''' |
1333 | + Gets the stored data given the secondary key |
1334 | + |
1335 | + @param secondary: the primary key |
1336 | + @returns object: the stored object |
1337 | + ''' |
1338 | + primary = self._key_to_key_bidict._get_by_second(secondary) |
1339 | + return self._get_by_primary(primary) |
1340 | + |
1341 | + def _remove_by_primary(self, primary): |
1342 | + ''' |
1343 | + Removes a triplet given the rpimary key. |
1344 | + |
1345 | + @param primary: the primary key |
1346 | + ''' |
1347 | + del self._primary_to_value[primary] |
1348 | + self._key_to_key_bidict._remove_by_first(primary) |
1349 | + |
1350 | + def _remove_by_secondary(self, secondary): |
1351 | + ''' |
1352 | + Removes a triplet given the rpimary key. |
1353 | + |
1354 | + @param secondary: the primary key |
1355 | + ''' |
1356 | + primary = self._key_to_key_bidict._get_by_second(secondary) |
1357 | + self._remove_by_primary(primary) |
1358 | + |
1359 | + def _get_secondary_key(self, primary): |
1360 | + ''' |
1361 | + Gets the secondary key given the primary |
1362 | + |
1363 | + @param primary: the primary key |
1364 | + @returns object: the secondary key |
1365 | + ''' |
1366 | + return self._key_to_key_bidict._get_by_first(primary) |
1367 | + |
1368 | + def _get_primary_key(self, secondary): |
1369 | + ''' |
1370 | + Gets the primary key given the secondary |
1371 | + |
1372 | + @param secondary: the secondary key |
1373 | + @returns object: the primary key |
1374 | + ''' |
1375 | + return self._key_to_key_bidict._get_by_second(secondary) |
1376 | + |
1377 | + def _get_all_primary_keys(self): |
1378 | + ''' |
1379 | + Returns all primary keys |
1380 | + |
1381 | + @returns list: list of all primary keys |
1382 | + ''' |
1383 | + return self._key_to_key_bidict._get_all_first() |
1384 | + |
1385 | + def _get_all_secondary_keys(self): |
1386 | + ''' |
1387 | + Returns all secondary keys |
1388 | + |
1389 | + @returns list: list of all secondary keys |
1390 | + ''' |
1391 | + return self._key_to_key_bidict._get_all_second() |
1392 | + |
1393 | |
1394 | === added file 'GTG/tools/watchdog.py' |
1395 | --- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000 |
1396 | +++ GTG/tools/watchdog.py 2010-08-25 16:29:45 +0000 |
1397 | @@ -0,0 +1,53 @@ |
1398 | +# -*- coding: utf-8 -*- |
1399 | +# ----------------------------------------------------------------------------- |
1400 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
1401 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1402 | +# |
1403 | +# This program is free software: you can redistribute it and/or modify it under |
1404 | +# the terms of the GNU General Public License as published by the Free Software |
1405 | +# Foundation, either version 3 of the License, or (at your option) any later |
1406 | +# version. |
1407 | +# |
1408 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1409 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1410 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1411 | +# details. |
1412 | +# |
1413 | +# You should have received a copy of the GNU General Public License along with |
1414 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1415 | +# ----------------------------------------------------------------------------- |
1416 | +import threading |
1417 | + |
1418 | +class Watchdog(object): |
1419 | + ''' |
1420 | + a simple thread-safe watchdog. |
1421 | + usage: |
1422 | + with Watchdod(timeout, error_function): |
1423 | + #do something |
1424 | + ''' |
1425 | + |
1426 | + def __init__(self, timeout, error_function): |
1427 | + ''' |
1428 | + Just sets the timeout and the function to execute when an error occours |
1429 | + |
1430 | + @param timeout: timeout in seconds |
1431 | + @param error_function: what to execute in case the watchdog timer |
1432 | + triggers |
1433 | + ''' |
1434 | + self.timeout = timeout |
1435 | + self.error_function = error_function |
1436 | + |
1437 | + def __enter__(self): |
1438 | + '''Starts the countdown''' |
1439 | + self.timer = threading.Timer(self.timeout, self.error_function) |
1440 | + self.timer.start() |
1441 | + |
1442 | + def __exit__(self, type, value, traceback): |
1443 | + '''Aborts the countdown''' |
1444 | + try: |
1445 | + self.timer.cancel() |
1446 | + except: |
1447 | + pass |
1448 | + if value == None: |
1449 | + return True |
1450 | + return False |