GTG

Merge lp:~gtg-user/gtg/backends-utils into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
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
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.

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).

To post a comment you must log in.
lp:~gtg-user/gtg/backends-utils updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: