GTG

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

Proposed by Luca Invernizzi
Status: Superseded
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: 1207 lines (+1147/-0)
11 files modified
CHANGELOG (+1/-0)
GTG/backends/periodicimportbackend.py (+90/-0)
GTG/backends/syncengine.py (+288/-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+32278@code.launchpad.net

This proposal has been superseded by a proposal from 2010-08-13.

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
877. By Luca Invernizzi

merge with trunk

878. By Luca Invernizzi

handy initializer for SyncMeme

879. By Luca Invernizzi

updated changelog

880. By Luca Invernizzi

merge with trunk

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

Unmerged revisions

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-13 23:43:06 +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-13 23:43:06 +0000
16@@ -0,0 +1,90 @@
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+ @interruptible
65+ def start_get_tasks(self):
66+ '''
67+ This function launches the first periodic import, and schedules the
68+ next ones.
69+ '''
70+ try:
71+ if self.import_timer:
72+ self.import_timer.cancel()
73+ except:
74+ pass
75+ self._start_get_tasks()
76+ self.cancellation_point()
77+ if self.is_enabled() == False:
78+ return
79+ self.import_timer = threading.Timer( \
80+ self._parameters['period'] * 60.0, \
81+ self.start_get_tasks)
82+ self.import_timer.start()
83+
84+ def _start_get_tasks(self):
85+ '''
86+ This function executes an imports and schedules the next
87+ '''
88+ self.cancellation_point()
89+ BackendSignals().backend_sync_started(self.get_id())
90+ self.do_periodic_import()
91+ BackendSignals().backend_sync_ended(self.get_id())
92+
93+ def quit(self, disable = False):
94+ '''
95+ Called when GTG quits or disconnects the backend.
96+ '''
97+ super(PeriodicImportBackend, self).quit(disable)
98+ try:
99+ self.import_timer.cancel()
100+ except Exception:
101+ pass
102+ try:
103+ self.import_timer.join()
104+ except Exception:
105+ pass
106+
107
108=== added file 'GTG/backends/syncengine.py'
109--- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000
110+++ GTG/backends/syncengine.py 2010-08-13 23:43:06 +0000
111@@ -0,0 +1,288 @@
112+# -*- coding: utf-8 -*-
113+# -----------------------------------------------------------------------------
114+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
115+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
116+#
117+# This program is free software: you can redistribute it and/or modify it under
118+# the terms of the GNU General Public License as published by the Free Software
119+# Foundation, either version 3 of the License, or (at your option) any later
120+# version.
121+#
122+# This program is distributed in the hope that it will be useful, but WITHOUT
123+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
124+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
125+# details.
126+#
127+# You should have received a copy of the GNU General Public License along with
128+# this program. If not, see <http://www.gnu.org/licenses/>.
129+# -----------------------------------------------------------------------------
130+
131+'''
132+This library deals with synchronizing two sets of objects.
133+It works like this:
134+ - We have two sets of generic objects (local and remote)
135+ - We present one object of either one of the sets and ask the library what's
136+ the state of its synchronization
137+ - the library will tell us if we need to add a clone object in the other set,
138+ update it or, if the other one has been removed, remove also this one
139+'''
140+from GTG.tools.twokeydict import TwoKeyDict
141+
142+
143+TYPE_LOCAL = "local"
144+TYPE_REMOTE = "remote"
145+
146+
147+
148+class SyncMeme(object):
149+ '''
150+ A SyncMeme is the object storing the data needed to keep track of the state
151+ of two objects synchronization.
152+ This basic version, that can be expanded as needed by the code using the
153+ SyncEngine, just stores the modified date and time of the last
154+ synchronization for both objects (local and remote)
155+ '''
156+ #NOTE: Checking objects CRCs would make this check nicer, as we could know
157+ # if the object was really changed, or it has just updated its
158+ # modified time (invernizzi)
159+
160+ def __init__(self,
161+ local_modified = None,
162+ remote_modified = None,
163+ origin = None):
164+ '''
165+ Creates a new SyncMeme, updating the modified times for both the
166+ local and remote objects, and sets the given origin.
167+ If any of the parameters is set to None, it's ignored.
168+
169+ @param local_modified: the modified time for the local object
170+ @param remote_modified: the modified time for the remote object
171+ @param origin: an object that identifies whether the local or the remote is
172+ the original object, the other one being a copy.
173+ '''
174+ if local_modified != None:
175+ self.set_local_last_modified(local_modified)
176+ if remote_modified != None:
177+ self.set_remote_last_modified(remote_modified)
178+ if origin != None:
179+ self.set_origin(origin)
180+
181+ def set_local_last_modified(self, modified_datetime):
182+ '''
183+ Setter function for the local object modified datetime.
184+
185+ @param modified_datetime: the local object modified datetime
186+ '''
187+ self.local_last_modified = modified_datetime
188+
189+ def get_local_last_modified(self):
190+ '''
191+ Getter function for the local object modified datetime.
192+ '''
193+ return self.local_last_modified
194+
195+ def set_remote_last_modified(self, modified_datetime):
196+ '''
197+ Setter function for the remote object modified datetime.
198+
199+ @param modified_datetime: the remote object modified datetime
200+ '''
201+ self.remote_last_modified = modified_datetime
202+
203+ def get_remote_last_modified(self):
204+ '''
205+ Getter function for the remote object modified datetime.
206+ '''
207+ return self.remote_last_modified
208+
209+ def which_is_newest(self, local_modified, remote_modified):
210+ '''
211+ Given the updated modified time for both the local and the remote
212+ objects, it checks them against the stored modified times and
213+ then against each other.
214+
215+ @returns string: "local"- if the local object has been modified and its
216+ the newest
217+ "remote" - the same for the remote object
218+ None - if no object modified time is newer than the
219+ stored one (the objects have not been modified)
220+ '''
221+ if local_modified <= self.local_last_modified and \
222+ remote_modified <= self.remote_last_modified:
223+ return None
224+ if local_modified > remote_modified:
225+ return "local"
226+ else:
227+ return "remote"
228+
229+ def get_origin(self):
230+ '''
231+ Returns the name of the source that firstly presented the object
232+ '''
233+ return self.origin
234+
235+ def set_origin(self, origin):
236+ '''
237+ Sets the source that presented the object for the first time. This
238+ source holds the original object, while the other holds the copy.
239+ This can be useful in the case of "lost syncability" (see the SyncEngine
240+ for an explaination).
241+
242+ @param origin: object representing the source
243+ '''
244+ self.origin = origin
245+
246+
247+
248+class SyncMemes(TwoKeyDict):
249+ '''
250+ A TwoKeyDict, with just the names changed to be better understandable.
251+ The meaning of these names is explained in the SyncEngine class description.
252+ It's used to store a set of SyncMeme objects, each one keeping storing all
253+ the data needed to keep track of a single relationship.
254+ '''
255+
256+
257+ get_remote_id = TwoKeyDict._get_secondary_key
258+ get_local_id = TwoKeyDict._get_primary_key
259+ remove_local_id = TwoKeyDict._remove_by_primary
260+ remove_remote_id = TwoKeyDict._remove_by_secondary
261+ get_meme_from_local_id = TwoKeyDict._get_by_primary
262+ get_meme_from_remote_id = TwoKeyDict._get_by_secondary
263+ get_all_local = TwoKeyDict._get_all_primary_keys
264+ get_all_remote = TwoKeyDict._get_all_secondary_keys
265+
266+
267+
268+class SyncEngine(object):
269+ '''
270+ The SyncEngine is an object useful in keeping two sets of objects
271+ synchronized.
272+ One set is called the Local set, the other is the Remote one.
273+ It stores the state of the synchronization and the latest state of each
274+ object.
275+ When asked, it can tell if a couple of related objects are up to date in the
276+ sync and, if not, which one must be updated.
277+
278+ It stores the state of each relationship in a series of SyncMeme.
279+ '''
280+
281+
282+ UPDATE = "update"
283+ REMOVE = "remove"
284+ ADD = "add"
285+ LOST_SYNCABILITY = "lost syncability"
286+
287+ def __init__(self):
288+ '''
289+ Initializes the storage of object relationships.
290+ '''
291+ self.sync_memes = SyncMemes()
292+
293+ def _analyze_element(self,
294+ element_id,
295+ is_local,
296+ has_local,
297+ has_remote,
298+ is_syncable = True):
299+ '''
300+ Given an object that should be synced with another one,
301+ it finds out about the related object, and decides whether:
302+ - the other object hasn't been created yet (thus must be added)
303+ - the other object has been deleted (thus this one must be deleted)
304+ - the other object is present, but either one has been changed
305+
306+ A particular case happens if the other object is present, but the
307+ "is_syncable" parameter (which tells that we intend to keep these two
308+ objects in sync) is set to False. In this case, this function returns
309+ that the Syncability property has been lost. This case is interesting if
310+ we want to delete one of the two objects (the one that has been cloned
311+ from the original).
312+
313+ @param element_id: the id of the element we're analysing.
314+ @param is_local: True if the element analysed is the local one (not the
315+ remote)
316+ @param has_local: function that accepts an id of the local set and
317+ returns True if the element is present
318+ @param has_remote: function that accepts an id of the remote set and
319+ returns True if the element is present
320+ @param is_syncable: explained above
321+ @returns string: one of self.UPDATE, self.ADD, self.REMOVE,
322+ self.LOST_SYNCABILITY
323+ '''
324+ if is_local:
325+ get_other_id = self.sync_memes.get_remote_id
326+ is_task_present = has_remote
327+ else:
328+ get_other_id = self.sync_memes.get_local_id
329+ is_task_present = has_local
330+
331+ try:
332+ other_id = get_other_id(element_id)
333+ if is_task_present(other_id):
334+ if is_syncable:
335+ return self.UPDATE, other_id
336+ else:
337+ return self.LOST_SYNCABILITY, other_id
338+ else:
339+ return self.REMOVE, None
340+ except KeyError:
341+ if is_syncable:
342+ return self.ADD, None
343+ return None, None
344+
345+ def analyze_local_id(self, element_id, *other_args):
346+ '''
347+ Shortcut to call _analyze_element for a local element
348+ '''
349+ return self._analyze_element(element_id, True, *other_args)
350+
351+ def analyze_remote_id(self, element_id, *other_args):
352+ '''
353+ Shortcut to call _analyze_element for a remote element
354+ '''
355+ return self._analyze_element(element_id, False, *other_args)
356+
357+ def record_relationship(self, local_id, remote_id, meme):
358+ '''
359+ Records that an object from the local set is related with one a remote
360+ set.
361+
362+ @param local_id: the id of the local task
363+ @param remote_id: the id of the remote task
364+ @param meme: the SyncMeme that keeps track of the relationship
365+ '''
366+ triplet = (local_id, remote_id, meme)
367+ self.sync_memes.add(triplet)
368+
369+ def break_relationship(self, local_id = None, remote_id = None):
370+ '''
371+ breaks a relationship between two objects.
372+ Only one of the two parameters is necessary to identify the
373+ relationship.
374+
375+ @param local_id: the id of the local task
376+ @param remote_id: the id of the remote task
377+ '''
378+ if local_id:
379+ self.sync_memes.remove_local_id(local_id)
380+ elif remote_id:
381+ self.sync_memes.remove_remote_id(remote_id)
382+
383+ def __getattr__(self, attr):
384+ '''
385+ The functions listed here are passed directly to the SyncMeme object
386+
387+ @param attr: a function name among the ones listed here
388+ @returns object: the function return object.
389+ '''
390+ if attr in ['get_remote_id',
391+ 'get_local_id',
392+ 'get_meme_from_local_id',
393+ 'get_meme_from_remote_id',
394+ 'get_all_local',
395+ 'get_all_remote']:
396+ return getattr(self.sync_memes, attr)
397+ else:
398+ raise AttributeError
399+
400
401=== added file 'GTG/tests/test_bidict.py'
402--- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000
403+++ GTG/tests/test_bidict.py 2010-08-13 23:43:06 +0000
404@@ -0,0 +1,79 @@
405+# -*- coding: utf-8 -*-
406+# -----------------------------------------------------------------------------
407+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
408+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
409+#
410+# This program is free software: you can redistribute it and/or modify it under
411+# the terms of the GNU General Public License as published by the Free Software
412+# Foundation, either version 3 of the License, or (at your option) any later
413+# version.
414+#
415+# This program is distributed in the hope that it will be useful, but WITHOUT
416+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
417+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
418+# details.
419+#
420+# You should have received a copy of the GNU General Public License along with
421+# this program. If not, see <http://www.gnu.org/licenses/>.
422+# -----------------------------------------------------------------------------
423+
424+'''
425+Tests for the diDict class
426+'''
427+
428+import unittest
429+import uuid
430+
431+from GTG.tools.bidict import BiDict
432+
433+
434+
435+class TestBiDict(unittest.TestCase):
436+ '''
437+ Tests for the BiDict object.
438+ '''
439+
440+
441+ def test_add_and_gets(self):
442+ '''
443+ Test for the __init__, _get_by_first, _get_by_second function
444+ '''
445+ pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)]
446+ bidict = BiDict(*pairs)
447+ for pair in pairs:
448+ self.assertEqual(bidict._get_by_first(pair[0]), pair[1])
449+ self.assertEqual(bidict._get_by_second(pair[1]), pair[0])
450+
451+ def test_remove_by_first_or_second(self):
452+ '''
453+ Tests for removing elements from the biDict
454+ '''
455+ pair_first = (1, 'one')
456+ pair_second = (2, 'two')
457+ bidict = BiDict(pair_first, pair_second)
458+ bidict._remove_by_first(pair_first[0])
459+ bidict._remove_by_second(pair_second[1])
460+ missing_first = 0
461+ missing_second = 0
462+ try:
463+ bidict._get_by_first(pair_first[0])
464+ except KeyError:
465+ missing_first += 1
466+ try:
467+ bidict._get_by_first(pair_second[0])
468+ except KeyError:
469+ missing_first += 1
470+ try:
471+ bidict._get_by_second(pair_first[1])
472+ except KeyError:
473+ missing_second += 1
474+ try:
475+ bidict._get_by_second(pair_second[1])
476+ except KeyError:
477+ missing_second += 1
478+ self.assertEqual(missing_first, 2)
479+ self.assertEqual(missing_second, 2)
480+
481+def test_suite():
482+ return unittest.TestLoader().loadTestsFromTestCase(TestBiDict)
483+
484
485=== added file 'GTG/tests/test_dates.py'
486--- GTG/tests/test_dates.py 1970-01-01 00:00:00 +0000
487+++ GTG/tests/test_dates.py 2010-08-13 23:43:06 +0000
488@@ -0,0 +1,43 @@
489+# -*- coding: utf-8 -*-
490+# -----------------------------------------------------------------------------
491+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
492+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
493+#
494+# This program is free software: you can redistribute it and/or modify it under
495+# the terms of the GNU General Public License as published by the Free Software
496+# Foundation, either version 3 of the License, or (at your option) any later
497+# version.
498+#
499+# This program is distributed in the hope that it will be useful, but WITHOUT
500+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
501+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
502+# details.
503+#
504+# You should have received a copy of the GNU General Public License along with
505+# this program. If not, see <http://www.gnu.org/licenses/>.
506+# -----------------------------------------------------------------------------
507+
508+'''
509+Tests for the various Date classes
510+'''
511+
512+import unittest
513+
514+from GTG.tools.dates import get_canonical_date
515+
516+class TestDates(unittest.TestCase):
517+ '''
518+ Tests for the various Date classes
519+ '''
520+
521+ def test_get_canonical_date(self):
522+ '''
523+ Tests for "get_canonical_date"
524+ '''
525+ for str in ["1985-03-29", "now", "soon", "later", ""]:
526+ date = get_canonical_date(str)
527+ self.assertEqual(date.__str__(), str)
528+
529+def test_suite():
530+ return unittest.TestLoader().loadTestsFromTestCase(TestDates)
531+
532
533=== added file 'GTG/tests/test_syncengine.py'
534--- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000
535+++ GTG/tests/test_syncengine.py 2010-08-13 23:43:06 +0000
536@@ -0,0 +1,189 @@
537+# -*- coding: utf-8 -*-
538+# -----------------------------------------------------------------------------
539+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
540+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
541+#
542+# This program is free software: you can redistribute it and/or modify it under
543+# the terms of the GNU General Public License as published by the Free Software
544+# Foundation, either version 3 of the License, or (at your option) any later
545+# version.
546+#
547+# This program is distributed in the hope that it will be useful, but WITHOUT
548+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
549+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
550+# details.
551+#
552+# You should have received a copy of the GNU General Public License along with
553+# this program. If not, see <http://www.gnu.org/licenses/>.
554+# -----------------------------------------------------------------------------
555+
556+'''
557+Tests for the SyncEngine class
558+'''
559+
560+import unittest
561+import uuid
562+
563+from GTG.backends.syncengine import SyncEngine
564+
565+
566+
567+class TestSyncEngine(unittest.TestCase):
568+ '''
569+ Tests for the SyncEngine object.
570+ '''
571+
572+ def setUp(self):
573+ self.ftp_local = FakeTaskProvider()
574+ self.ftp_remote = FakeTaskProvider()
575+ self.sync_engine = SyncEngine()
576+
577+ def test_analyze_element_and_record_and_break_relationship(self):
578+ '''
579+ Test for the _analyze_element, analyze_remote_id, analyze_local_id,
580+ record_relationship, break_relationship
581+ '''
582+ #adding a new local task
583+ local_id = uuid.uuid4()
584+ self.ftp_local.fake_add_task(local_id)
585+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
586+ self.ftp_local.has_task, self.ftp_remote.has_task), \
587+ (SyncEngine.ADD, None))
588+ #creating the related remote task
589+ remote_id = uuid.uuid4()
590+ self.ftp_remote.fake_add_task(remote_id)
591+ #informing the sync_engine about that
592+ self.sync_engine.record_relationship(local_id, remote_id, object())
593+ #verifying that it understood that
594+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
595+ self.ftp_local.has_task, self.ftp_remote.has_task), \
596+ (SyncEngine.UPDATE, remote_id))
597+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
598+ self.ftp_local.has_task, self.ftp_remote.has_task), \
599+ (SyncEngine.UPDATE, local_id))
600+ #and not the reverse
601+ self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \
602+ self.ftp_local.has_task, self.ftp_remote.has_task), \
603+ (SyncEngine.ADD, None))
604+ self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \
605+ self.ftp_local.has_task, self.ftp_remote.has_task), \
606+ (SyncEngine.ADD, None))
607+ #now we remove the remote task
608+ self.ftp_remote.fake_remove_task(remote_id)
609+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
610+ self.ftp_local.has_task, self.ftp_remote.has_task), \
611+ (SyncEngine.REMOVE, None))
612+ self.sync_engine.break_relationship(local_id = local_id)
613+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
614+ self.ftp_local.has_task, self.ftp_remote.has_task), \
615+ (SyncEngine.ADD, None))
616+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
617+ self.ftp_local.has_task, self.ftp_remote.has_task), \
618+ (SyncEngine.ADD, None))
619+ #we add them back and remove giving the remote id as key to find what to
620+ #delete
621+ self.ftp_local.fake_add_task(local_id)
622+ self.ftp_remote.fake_add_task(remote_id)
623+ self.ftp_remote.fake_remove_task(remote_id)
624+ self.sync_engine.record_relationship(local_id, remote_id, object)
625+ self.sync_engine.break_relationship(remote_id = remote_id)
626+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
627+ self.ftp_local.has_task, self.ftp_remote.has_task), \
628+ (SyncEngine.ADD, None))
629+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
630+ self.ftp_local.has_task, self.ftp_remote.has_task), \
631+ (SyncEngine.ADD, None))
632+
633+ def test_syncability(self):
634+ '''
635+ Test for the _analyze_element, analyze_remote_id, analyze_local_id.
636+ Checks that the is_syncable parameter is used correctly
637+ '''
638+ #adding a new local task unsyncable
639+ local_id = uuid.uuid4()
640+ self.ftp_local.fake_add_task(local_id)
641+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
642+ self.ftp_local.has_task, self.ftp_remote.has_task,
643+ False), \
644+ (None, None))
645+ #adding a new local task, syncable
646+ local_id = uuid.uuid4()
647+ self.ftp_local.fake_add_task(local_id)
648+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
649+ self.ftp_local.has_task, self.ftp_remote.has_task), \
650+ (SyncEngine.ADD, None))
651+ #creating the related remote task
652+ remote_id = uuid.uuid4()
653+ self.ftp_remote.fake_add_task(remote_id)
654+ #informing the sync_engine about that
655+ self.sync_engine.record_relationship(local_id, remote_id, object())
656+ #checking that it behaves correctly with established relationships
657+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
658+ self.ftp_local.has_task, self.ftp_remote.has_task,
659+ True), \
660+ (SyncEngine.UPDATE, remote_id))
661+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
662+ self.ftp_local.has_task, self.ftp_remote.has_task,
663+ False), \
664+ (SyncEngine.LOST_SYNCABILITY, remote_id))
665+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
666+ self.ftp_local.has_task, self.ftp_remote.has_task,
667+ True), \
668+ (SyncEngine.UPDATE, local_id))
669+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
670+ self.ftp_local.has_task, self.ftp_remote.has_task,
671+ False), \
672+ (SyncEngine.LOST_SYNCABILITY, local_id))
673+ #now we remove the remote task
674+ self.ftp_remote.fake_remove_task(remote_id)
675+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
676+ self.ftp_local.has_task, self.ftp_remote.has_task,
677+ True), \
678+ (SyncEngine.REMOVE, None))
679+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
680+ self.ftp_local.has_task, self.ftp_remote.has_task,
681+ False), \
682+ (SyncEngine.REMOVE, None))
683+ self.sync_engine.break_relationship(local_id = local_id)
684+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
685+ self.ftp_local.has_task, self.ftp_remote.has_task,
686+ True), \
687+ (SyncEngine.ADD, None))
688+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
689+ self.ftp_local.has_task, self.ftp_remote.has_task,
690+ False), \
691+ (None, None))
692+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
693+ self.ftp_local.has_task, self.ftp_remote.has_task,
694+ True), \
695+ (SyncEngine.ADD, None))
696+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
697+ self.ftp_local.has_task, self.ftp_remote.has_task,
698+ False), \
699+ (None, None))
700+
701+def test_suite():
702+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine)
703+
704+
705+class FakeTaskProvider(object):
706+
707+ def __init__(self):
708+ self.dic = {}
709+
710+ def has_task(self, tid):
711+ return self.dic.has_key(tid)
712+
713+###############################################################################
714+### Function with the fake_ prefix are here to assist in testing, they do not
715+### need to be present in the real class
716+###############################################################################
717+
718+ def fake_add_task(self, tid):
719+ self.dic[tid] = "something"
720+
721+ def fake_get_task(self, tid):
722+ return self.dic[tid]
723+
724+ def fake_remove_task(self, tid):
725+ del self.dic[tid]
726
727=== added file 'GTG/tests/test_syncmeme.py'
728--- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000
729+++ GTG/tests/test_syncmeme.py 2010-08-13 23:43:06 +0000
730@@ -0,0 +1,59 @@
731+# -*- coding: utf-8 -*-
732+# -----------------------------------------------------------------------------
733+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
734+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
735+#
736+# This program is free software: you can redistribute it and/or modify it under
737+# the terms of the GNU General Public License as published by the Free Software
738+# Foundation, either version 3 of the License, or (at your option) any later
739+# version.
740+#
741+# This program is distributed in the hope that it will be useful, but WITHOUT
742+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
743+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
744+# details.
745+#
746+# You should have received a copy of the GNU General Public License along with
747+# this program. If not, see <http://www.gnu.org/licenses/>.
748+# -----------------------------------------------------------------------------
749+
750+'''
751+Tests for the SyncMeme class
752+'''
753+
754+import unittest
755+import datetime
756+
757+from GTG.backends.syncengine import SyncMeme
758+
759+
760+
761+class TestSyncMeme(unittest.TestCase):
762+ '''
763+ Tests for the SyncEngine object.
764+ '''
765+
766+ def test_which_is_newest(self):
767+ '''
768+ test the which_is_newest function
769+
770+ '''
771+ meme = SyncMeme()
772+ #tasks have not changed
773+ local_modified = datetime.datetime.now()
774+ remote_modified = datetime.datetime.now()
775+ meme.set_local_last_modified(local_modified)
776+ meme.set_remote_last_modified(remote_modified)
777+ self.assertEqual(meme.which_is_newest(local_modified, \
778+ remote_modified), None)
779+ #we update the local
780+ local_modified = datetime.datetime.now()
781+ self.assertEqual(meme.which_is_newest(local_modified, \
782+ remote_modified), 'local')
783+ #we update the remote
784+ remote_modified = datetime.datetime.now()
785+ self.assertEqual(meme.which_is_newest(local_modified, \
786+ remote_modified), 'remote')
787+def test_suite():
788+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme)
789+
790
791=== added file 'GTG/tests/test_twokeydict.py'
792--- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000
793+++ GTG/tests/test_twokeydict.py 2010-08-13 23:43:06 +0000
794@@ -0,0 +1,98 @@
795+# -*- coding: utf-8 -*-
796+# -----------------------------------------------------------------------------
797+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
798+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
799+#
800+# This program is free software: you can redistribute it and/or modify it under
801+# the terms of the GNU General Public License as published by the Free Software
802+# Foundation, either version 3 of the License, or (at your option) any later
803+# version.
804+#
805+# This program is distributed in the hope that it will be useful, but WITHOUT
806+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
807+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
808+# details.
809+#
810+# You should have received a copy of the GNU General Public License along with
811+# this program. If not, see <http://www.gnu.org/licenses/>.
812+# -----------------------------------------------------------------------------
813+
814+'''
815+Tests for the TwoKeyDict class
816+'''
817+
818+import unittest
819+import uuid
820+
821+from GTG.tools.twokeydict import TwoKeyDict
822+
823+
824+
825+class TestTwoKeyDict(unittest.TestCase):
826+ '''
827+ Tests for the TwoKeyDict object.
828+ '''
829+
830+
831+ def test_add_and_gets(self):
832+ '''
833+ Test for the __init__, _get_by_first, _get_by_second function
834+ '''
835+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
836+ for a in xrange(10)]
837+ tw_dict = TwoKeyDict(*triplets)
838+ for triplet in triplets:
839+ self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2])
840+ self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2])
841+
842+ def test_remove_by_first_or_second(self):
843+ '''
844+ Test for removing triplets form the TwoKeyDict
845+ '''
846+ triplet_first = (1, 'I', 'one')
847+ triplet_second = (2, 'II', 'two')
848+ tw_dict = TwoKeyDict(triplet_first, triplet_second)
849+ tw_dict._remove_by_primary(triplet_first[0])
850+ tw_dict._remove_by_secondary(triplet_second[1])
851+ missing_first = 0
852+ missing_second = 0
853+ try:
854+ tw_dict._get_by_primary(triplet_first[0])
855+ except KeyError:
856+ missing_first += 1
857+ try:
858+ tw_dict._get_by_secondary(triplet_second[0])
859+ except KeyError:
860+ missing_first += 1
861+ try:
862+ tw_dict._get_by_secondary(triplet_first[1])
863+ except KeyError:
864+ missing_second += 1
865+ try:
866+ tw_dict._get_by_secondary(triplet_second[1])
867+ except KeyError:
868+ missing_second += 1
869+ self.assertEqual(missing_first, 2)
870+ self.assertEqual(missing_second, 2)
871+ #check for memory leaks
872+ dict_len = 0
873+ for key in tw_dict._primary_to_value.iterkeys():
874+ dict_len += 1
875+ self.assertEqual(dict_len, 0)
876+
877+ def test_get_primary_and_secondary_key(self):
878+ '''
879+ Test for fetching the objects stored in the TwoKeyDict
880+ '''
881+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
882+ for a in xrange(10)]
883+ tw_dict = TwoKeyDict(*triplets)
884+ for triplet in triplets:
885+ self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \
886+ triplet[1])
887+ self.assertEqual(tw_dict._get_primary_key(triplet[1]), \
888+ triplet[0])
889+
890+def test_suite():
891+ return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict)
892+
893
894=== added file 'GTG/tools/bidict.py'
895--- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000
896+++ GTG/tools/bidict.py 2010-08-13 23:43:06 +0000
897@@ -0,0 +1,112 @@
898+# -*- coding: utf-8 -*-
899+# -----------------------------------------------------------------------------
900+# Getting Things Gnome! - a personal organizer for the GNOME desktop
901+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
902+#
903+# This program is free software: you can redistribute it and/or modify it under
904+# the terms of the GNU General Public License as published by the Free Software
905+# Foundation, either version 3 of the License, or (at your option) any later
906+# version.
907+#
908+# This program is distributed in the hope that it will be useful, but WITHOUT
909+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
910+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
911+# details.
912+#
913+# You should have received a copy of the GNU General Public License along with
914+# this program. If not, see <http://www.gnu.org/licenses/>.
915+# -----------------------------------------------------------------------------
916+
917+
918+
919+class BiDict(object):
920+ '''
921+ Bidirectional dictionary: the pairs stored can be accessed using either the
922+ first or the second element as key (named key1 and key2).
923+ You don't need this if there is no clash between the domains of the first
924+ and second element of the pairs.
925+ '''
926+
927+ def __init__(self, *pairs):
928+ '''
929+ Initialization of the bidirectional dictionary
930+
931+ @param pairs: optional. A list of pairs to add to the dictionary
932+ '''
933+ super(BiDict, self).__init__()
934+ self._first_to_second = {}
935+ self._second_to_first = {}
936+ for pair in pairs:
937+ self.add(pair)
938+
939+ def add(self, pair):
940+ '''
941+ Adds a pair (key1, key2) to the dictionary
942+
943+ @param pair: the pair formatted as (key1, key2)
944+ '''
945+ self._first_to_second[pair[0]] = pair[1]
946+ self._second_to_first[pair[1]] = pair[0]
947+
948+ def _get_by_first(self, key):
949+ '''
950+ Gets the key2 given key1
951+
952+ @param key: the first key
953+ '''
954+ return self._first_to_second[key]
955+
956+ def _get_by_second(self, key):
957+ '''
958+ Gets the key1 given key2
959+
960+ @param key: the second key
961+ '''
962+ return self._second_to_first[key]
963+
964+ def _remove_by_first(self, first):
965+ '''
966+ Removes a pair given the first key
967+
968+ @param key: the first key
969+ '''
970+ second = self._first_to_second[first]
971+ del self._second_to_first[second]
972+ del self._first_to_second[first]
973+
974+ def _remove_by_second(self, second):
975+ '''
976+ Removes a pair given the second key
977+
978+ @param key: the second key
979+ '''
980+ first = self._second_to_first[second]
981+ del self._first_to_second[first]
982+ del self._second_to_first[second]
983+
984+ def _get_all_first(self):
985+ '''
986+ Returns the list of all first keys
987+
988+ @returns list
989+ '''
990+ return list(self._first_to_second)
991+
992+ def _get_all_second(self):
993+ '''
994+ Returns the list of all second keys
995+
996+ @returns list
997+ '''
998+ return list(self._second_to_first)
999+
1000+ def __str__(self):
1001+ '''
1002+ returns a string representing the content of this BiDict
1003+
1004+ @returns string
1005+ '''
1006+ return reduce(lambda text, keys: \
1007+ str(text) + str(keys),
1008+ self._first_to_second.iteritems())
1009+
1010
1011=== added file 'GTG/tools/twokeydict.py'
1012--- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000
1013+++ GTG/tools/twokeydict.py 2010-08-13 23:43:06 +0000
1014@@ -0,0 +1,135 @@
1015+# -*- coding: utf-8 -*-
1016+# -----------------------------------------------------------------------------
1017+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1018+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1019+#
1020+# This program is free software: you can redistribute it and/or modify it under
1021+# the terms of the GNU General Public License as published by the Free Software
1022+# Foundation, either version 3 of the License, or (at your option) any later
1023+# version.
1024+#
1025+# This program is distributed in the hope that it will be useful, but WITHOUT
1026+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1027+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1028+# details.
1029+#
1030+# You should have received a copy of the GNU General Public License along with
1031+# this program. If not, see <http://www.gnu.org/licenses/>.
1032+# -----------------------------------------------------------------------------
1033+
1034+'''
1035+Contains TwoKeyDict, a Dictionary which also has a secondary key
1036+'''
1037+
1038+from GTG.tools.bidict import BiDict
1039+
1040+
1041+
1042+class TwoKeyDict(object):
1043+ '''
1044+ It's a standard Dictionary with a secondary key.
1045+ For example, you can add an element ('2', 'II', two'), where the
1046+ first two arguments are keys and the third is the stored object, and access
1047+ it as:
1048+ twokey['2'] ==> 'two'
1049+ twokey['II'] ==> 'two'
1050+ You can also request the other key, given one.
1051+ Function calls start with _ because you'll probably want to rename them when
1052+ you use this dictionary, for the sake of clarity.
1053+ '''
1054+
1055+
1056+ def __init__(self, *triplets):
1057+ '''
1058+ Creates the TwoKeyDict and optionally populates it with some data
1059+
1060+ @oaram triplets: tuples for populating the TwoKeyDict. Format:
1061+ ((key1, key2, data_to_store), ...)
1062+ '''
1063+ super(TwoKeyDict, self).__init__()
1064+ self._key_to_key_bidict = BiDict()
1065+ self._primary_to_value = {}
1066+ for triplet in triplets:
1067+ self.add(triplet)
1068+
1069+ def add(self, triplet):
1070+ '''
1071+ Adds a new triplet to the TwoKeyDict
1072+
1073+ @param triplet: a tuple formatted like this:
1074+ (key1, key2, data_to_store)
1075+ '''
1076+ self._key_to_key_bidict.add((triplet[0], triplet[1]))
1077+ self._primary_to_value[triplet[0]] = triplet[2]
1078+
1079+ def _get_by_primary(self, primary):
1080+ '''
1081+ Gets the stored data given the primary key
1082+
1083+ @param primary: the primary key
1084+ @returns object: the stored object
1085+ '''
1086+ return self._primary_to_value[primary]
1087+
1088+ def _get_by_secondary(self, secondary):
1089+ '''
1090+ Gets the stored data given the secondary key
1091+
1092+ @param secondary: the primary key
1093+ @returns object: the stored object
1094+ '''
1095+ primary = self._key_to_key_bidict._get_by_second(secondary)
1096+ return self._get_by_primary(primary)
1097+
1098+ def _remove_by_primary(self, primary):
1099+ '''
1100+ Removes a triplet given the rpimary key.
1101+
1102+ @param primary: the primary key
1103+ '''
1104+ del self._primary_to_value[primary]
1105+ self._key_to_key_bidict._remove_by_first(primary)
1106+
1107+ def _remove_by_secondary(self, secondary):
1108+ '''
1109+ Removes a triplet given the rpimary key.
1110+
1111+ @param secondary: the primary key
1112+ '''
1113+ primary = self._key_to_key_bidict._get_by_second(secondary)
1114+ self._remove_by_primary(primary)
1115+
1116+ def _get_secondary_key(self, primary):
1117+ '''
1118+ Gets the secondary key given the primary
1119+
1120+ @param primary: the primary key
1121+ @returns object: the secondary key
1122+ '''
1123+ return self._key_to_key_bidict._get_by_first(primary)
1124+
1125+ def _get_primary_key(self, secondary):
1126+ '''
1127+ Gets the primary key given the secondary
1128+
1129+ @param secondary: the secondary key
1130+ @returns object: the primary key
1131+ '''
1132+ return self._key_to_key_bidict._get_by_second(secondary)
1133+
1134+ def _get_all_primary_keys(self):
1135+ '''
1136+ Returns all primary keys
1137+
1138+ @returns list: list of all primary keys
1139+ '''
1140+ return self._key_to_key_bidict._get_all_first()
1141+
1142+ def _get_all_secondary_keys(self):
1143+ '''
1144+ Returns all secondary keys
1145+
1146+ @returns list: list of all secondary keys
1147+ '''
1148+ return self._key_to_key_bidict._get_all_second()
1149+
1150
1151=== added file 'GTG/tools/watchdog.py'
1152--- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000
1153+++ GTG/tools/watchdog.py 2010-08-13 23:43:06 +0000
1154@@ -0,0 +1,53 @@
1155+# -*- coding: utf-8 -*-
1156+# -----------------------------------------------------------------------------
1157+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1158+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1159+#
1160+# This program is free software: you can redistribute it and/or modify it under
1161+# the terms of the GNU General Public License as published by the Free Software
1162+# Foundation, either version 3 of the License, or (at your option) any later
1163+# version.
1164+#
1165+# This program is distributed in the hope that it will be useful, but WITHOUT
1166+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1167+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1168+# details.
1169+#
1170+# You should have received a copy of the GNU General Public License along with
1171+# this program. If not, see <http://www.gnu.org/licenses/>.
1172+# -----------------------------------------------------------------------------
1173+import threading
1174+
1175+class Watchdog(object):
1176+ '''
1177+ a simple thread-safe watchdog.
1178+ usage:
1179+ with Watchdod(timeout, error_function):
1180+ #do something
1181+ '''
1182+
1183+ def __init__(self, timeout, error_function):
1184+ '''
1185+ Just sets the timeout and the function to execute when an error occours
1186+
1187+ @param timeout: timeout in seconds
1188+ @param error_function: what to execute in case the watchdog timer
1189+ triggers
1190+ '''
1191+ self.timeout = timeout
1192+ self.error_function = error_function
1193+
1194+ def __enter__(self):
1195+ '''Starts the countdown'''
1196+ self.timer = threading.Timer(self.timeout, self.error_function)
1197+ self.timer.start()
1198+
1199+ def __exit__(self, type, value, traceback):
1200+ '''Aborts the countdown'''
1201+ try:
1202+ self.timer.cancel()
1203+ except:
1204+ pass
1205+ if value == None:
1206+ return True
1207+ return False

Subscribers

People subscribed via source and target branches

to status/vote changes: