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
=== modified file 'CHANGELOG'
--- CHANGELOG 2010-08-04 00:30:22 +0000
+++ CHANGELOG 2010-08-13 23:43:06 +0000
@@ -4,6 +4,7 @@
4 * Fixed bug with data consistency #579189, by Marko Kevac4 * Fixed bug with data consistency #579189, by Marko Kevac
5 * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij5 * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
6 * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul6 * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
7 * Added utilities for complex backends by Luca Invernizzi
78
82010-03-01 Getting Things GNOME! 0.2.292010-03-01 Getting Things GNOME! 0.2.2
9 * Autostart on login, by Luca Invernizzi10 * Autostart on login, by Luca Invernizzi
1011
=== added file 'GTG/backends/periodicimportbackend.py'
--- GTG/backends/periodicimportbackend.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/periodicimportbackend.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,90 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Contains PeriodicImportBackend, a GenericBackend specialized for checking the
22remote backend in polling.
23'''
24
25import threading
26
27from GTG.backends.genericbackend import GenericBackend
28from GTG.backends.backendsignals import BackendSignals
29from GTG.tools.interruptible import interruptible
30
31
32
33class PeriodicImportBackend(GenericBackend):
34 '''
35 This class can be used in place of GenericBackend when a periodic import is
36 necessary, as the remote service providing tasks does not signals the
37 changes.
38 To use this, only two things are necessary:
39 - using do_periodic_import instead of start_get_tasks
40 - having in _static_parameters a "period" key, as in
41 "period": { \
42 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
43 GenericBackend.PARAM_DEFAULT_VALUE: 2, },
44 This specifies the time that must pass between consecutive imports
45 (in minutes)
46 '''
47
48 @interruptible
49 def start_get_tasks(self):
50 '''
51 This function launches the first periodic import, and schedules the
52 next ones.
53 '''
54 try:
55 if self.import_timer:
56 self.import_timer.cancel()
57 except:
58 pass
59 self._start_get_tasks()
60 self.cancellation_point()
61 if self.is_enabled() == False:
62 return
63 self.import_timer = threading.Timer( \
64 self._parameters['period'] * 60.0, \
65 self.start_get_tasks)
66 self.import_timer.start()
67
68 def _start_get_tasks(self):
69 '''
70 This function executes an imports and schedules the next
71 '''
72 self.cancellation_point()
73 BackendSignals().backend_sync_started(self.get_id())
74 self.do_periodic_import()
75 BackendSignals().backend_sync_ended(self.get_id())
76
77 def quit(self, disable = False):
78 '''
79 Called when GTG quits or disconnects the backend.
80 '''
81 super(PeriodicImportBackend, self).quit(disable)
82 try:
83 self.import_timer.cancel()
84 except Exception:
85 pass
86 try:
87 self.import_timer.join()
88 except Exception:
89 pass
90
091
=== added file 'GTG/backends/syncengine.py'
--- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/syncengine.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,288 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21This library deals with synchronizing two sets of objects.
22It works like this:
23 - We have two sets of generic objects (local and remote)
24 - We present one object of either one of the sets and ask the library what's
25 the state of its synchronization
26 - the library will tell us if we need to add a clone object in the other set,
27 update it or, if the other one has been removed, remove also this one
28'''
29from GTG.tools.twokeydict import TwoKeyDict
30
31
32TYPE_LOCAL = "local"
33TYPE_REMOTE = "remote"
34
35
36
37class SyncMeme(object):
38 '''
39 A SyncMeme is the object storing the data needed to keep track of the state
40 of two objects synchronization.
41 This basic version, that can be expanded as needed by the code using the
42 SyncEngine, just stores the modified date and time of the last
43 synchronization for both objects (local and remote)
44 '''
45 #NOTE: Checking objects CRCs would make this check nicer, as we could know
46 # if the object was really changed, or it has just updated its
47 # modified time (invernizzi)
48
49 def __init__(self,
50 local_modified = None,
51 remote_modified = None,
52 origin = None):
53 '''
54 Creates a new SyncMeme, updating the modified times for both the
55 local and remote objects, and sets the given origin.
56 If any of the parameters is set to None, it's ignored.
57
58 @param local_modified: the modified time for the local object
59 @param remote_modified: the modified time for the remote object
60 @param origin: an object that identifies whether the local or the remote is
61 the original object, the other one being a copy.
62 '''
63 if local_modified != None:
64 self.set_local_last_modified(local_modified)
65 if remote_modified != None:
66 self.set_remote_last_modified(remote_modified)
67 if origin != None:
68 self.set_origin(origin)
69
70 def set_local_last_modified(self, modified_datetime):
71 '''
72 Setter function for the local object modified datetime.
73
74 @param modified_datetime: the local object modified datetime
75 '''
76 self.local_last_modified = modified_datetime
77
78 def get_local_last_modified(self):
79 '''
80 Getter function for the local object modified datetime.
81 '''
82 return self.local_last_modified
83
84 def set_remote_last_modified(self, modified_datetime):
85 '''
86 Setter function for the remote object modified datetime.
87
88 @param modified_datetime: the remote object modified datetime
89 '''
90 self.remote_last_modified = modified_datetime
91
92 def get_remote_last_modified(self):
93 '''
94 Getter function for the remote object modified datetime.
95 '''
96 return self.remote_last_modified
97
98 def which_is_newest(self, local_modified, remote_modified):
99 '''
100 Given the updated modified time for both the local and the remote
101 objects, it checks them against the stored modified times and
102 then against each other.
103
104 @returns string: "local"- if the local object has been modified and its
105 the newest
106 "remote" - the same for the remote object
107 None - if no object modified time is newer than the
108 stored one (the objects have not been modified)
109 '''
110 if local_modified <= self.local_last_modified and \
111 remote_modified <= self.remote_last_modified:
112 return None
113 if local_modified > remote_modified:
114 return "local"
115 else:
116 return "remote"
117
118 def get_origin(self):
119 '''
120 Returns the name of the source that firstly presented the object
121 '''
122 return self.origin
123
124 def set_origin(self, origin):
125 '''
126 Sets the source that presented the object for the first time. This
127 source holds the original object, while the other holds the copy.
128 This can be useful in the case of "lost syncability" (see the SyncEngine
129 for an explaination).
130
131 @param origin: object representing the source
132 '''
133 self.origin = origin
134
135
136
137class SyncMemes(TwoKeyDict):
138 '''
139 A TwoKeyDict, with just the names changed to be better understandable.
140 The meaning of these names is explained in the SyncEngine class description.
141 It's used to store a set of SyncMeme objects, each one keeping storing all
142 the data needed to keep track of a single relationship.
143 '''
144
145
146 get_remote_id = TwoKeyDict._get_secondary_key
147 get_local_id = TwoKeyDict._get_primary_key
148 remove_local_id = TwoKeyDict._remove_by_primary
149 remove_remote_id = TwoKeyDict._remove_by_secondary
150 get_meme_from_local_id = TwoKeyDict._get_by_primary
151 get_meme_from_remote_id = TwoKeyDict._get_by_secondary
152 get_all_local = TwoKeyDict._get_all_primary_keys
153 get_all_remote = TwoKeyDict._get_all_secondary_keys
154
155
156
157class SyncEngine(object):
158 '''
159 The SyncEngine is an object useful in keeping two sets of objects
160 synchronized.
161 One set is called the Local set, the other is the Remote one.
162 It stores the state of the synchronization and the latest state of each
163 object.
164 When asked, it can tell if a couple of related objects are up to date in the
165 sync and, if not, which one must be updated.
166
167 It stores the state of each relationship in a series of SyncMeme.
168 '''
169
170
171 UPDATE = "update"
172 REMOVE = "remove"
173 ADD = "add"
174 LOST_SYNCABILITY = "lost syncability"
175
176 def __init__(self):
177 '''
178 Initializes the storage of object relationships.
179 '''
180 self.sync_memes = SyncMemes()
181
182 def _analyze_element(self,
183 element_id,
184 is_local,
185 has_local,
186 has_remote,
187 is_syncable = True):
188 '''
189 Given an object that should be synced with another one,
190 it finds out about the related object, and decides whether:
191 - the other object hasn't been created yet (thus must be added)
192 - the other object has been deleted (thus this one must be deleted)
193 - the other object is present, but either one has been changed
194
195 A particular case happens if the other object is present, but the
196 "is_syncable" parameter (which tells that we intend to keep these two
197 objects in sync) is set to False. In this case, this function returns
198 that the Syncability property has been lost. This case is interesting if
199 we want to delete one of the two objects (the one that has been cloned
200 from the original).
201
202 @param element_id: the id of the element we're analysing.
203 @param is_local: True if the element analysed is the local one (not the
204 remote)
205 @param has_local: function that accepts an id of the local set and
206 returns True if the element is present
207 @param has_remote: function that accepts an id of the remote set and
208 returns True if the element is present
209 @param is_syncable: explained above
210 @returns string: one of self.UPDATE, self.ADD, self.REMOVE,
211 self.LOST_SYNCABILITY
212 '''
213 if is_local:
214 get_other_id = self.sync_memes.get_remote_id
215 is_task_present = has_remote
216 else:
217 get_other_id = self.sync_memes.get_local_id
218 is_task_present = has_local
219
220 try:
221 other_id = get_other_id(element_id)
222 if is_task_present(other_id):
223 if is_syncable:
224 return self.UPDATE, other_id
225 else:
226 return self.LOST_SYNCABILITY, other_id
227 else:
228 return self.REMOVE, None
229 except KeyError:
230 if is_syncable:
231 return self.ADD, None
232 return None, None
233
234 def analyze_local_id(self, element_id, *other_args):
235 '''
236 Shortcut to call _analyze_element for a local element
237 '''
238 return self._analyze_element(element_id, True, *other_args)
239
240 def analyze_remote_id(self, element_id, *other_args):
241 '''
242 Shortcut to call _analyze_element for a remote element
243 '''
244 return self._analyze_element(element_id, False, *other_args)
245
246 def record_relationship(self, local_id, remote_id, meme):
247 '''
248 Records that an object from the local set is related with one a remote
249 set.
250
251 @param local_id: the id of the local task
252 @param remote_id: the id of the remote task
253 @param meme: the SyncMeme that keeps track of the relationship
254 '''
255 triplet = (local_id, remote_id, meme)
256 self.sync_memes.add(triplet)
257
258 def break_relationship(self, local_id = None, remote_id = None):
259 '''
260 breaks a relationship between two objects.
261 Only one of the two parameters is necessary to identify the
262 relationship.
263
264 @param local_id: the id of the local task
265 @param remote_id: the id of the remote task
266 '''
267 if local_id:
268 self.sync_memes.remove_local_id(local_id)
269 elif remote_id:
270 self.sync_memes.remove_remote_id(remote_id)
271
272 def __getattr__(self, attr):
273 '''
274 The functions listed here are passed directly to the SyncMeme object
275
276 @param attr: a function name among the ones listed here
277 @returns object: the function return object.
278 '''
279 if attr in ['get_remote_id',
280 'get_local_id',
281 'get_meme_from_local_id',
282 'get_meme_from_remote_id',
283 'get_all_local',
284 'get_all_remote']:
285 return getattr(self.sync_memes, attr)
286 else:
287 raise AttributeError
288
0289
=== added file 'GTG/tests/test_bidict.py'
--- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_bidict.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,79 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the diDict class
22'''
23
24import unittest
25import uuid
26
27from GTG.tools.bidict import BiDict
28
29
30
31class TestBiDict(unittest.TestCase):
32 '''
33 Tests for the BiDict object.
34 '''
35
36
37 def test_add_and_gets(self):
38 '''
39 Test for the __init__, _get_by_first, _get_by_second function
40 '''
41 pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)]
42 bidict = BiDict(*pairs)
43 for pair in pairs:
44 self.assertEqual(bidict._get_by_first(pair[0]), pair[1])
45 self.assertEqual(bidict._get_by_second(pair[1]), pair[0])
46
47 def test_remove_by_first_or_second(self):
48 '''
49 Tests for removing elements from the biDict
50 '''
51 pair_first = (1, 'one')
52 pair_second = (2, 'two')
53 bidict = BiDict(pair_first, pair_second)
54 bidict._remove_by_first(pair_first[0])
55 bidict._remove_by_second(pair_second[1])
56 missing_first = 0
57 missing_second = 0
58 try:
59 bidict._get_by_first(pair_first[0])
60 except KeyError:
61 missing_first += 1
62 try:
63 bidict._get_by_first(pair_second[0])
64 except KeyError:
65 missing_first += 1
66 try:
67 bidict._get_by_second(pair_first[1])
68 except KeyError:
69 missing_second += 1
70 try:
71 bidict._get_by_second(pair_second[1])
72 except KeyError:
73 missing_second += 1
74 self.assertEqual(missing_first, 2)
75 self.assertEqual(missing_second, 2)
76
77def test_suite():
78 return unittest.TestLoader().loadTestsFromTestCase(TestBiDict)
79
080
=== added file 'GTG/tests/test_dates.py'
--- GTG/tests/test_dates.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_dates.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,43 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the various Date classes
22'''
23
24import unittest
25
26from GTG.tools.dates import get_canonical_date
27
28class TestDates(unittest.TestCase):
29 '''
30 Tests for the various Date classes
31 '''
32
33 def test_get_canonical_date(self):
34 '''
35 Tests for "get_canonical_date"
36 '''
37 for str in ["1985-03-29", "now", "soon", "later", ""]:
38 date = get_canonical_date(str)
39 self.assertEqual(date.__str__(), str)
40
41def test_suite():
42 return unittest.TestLoader().loadTestsFromTestCase(TestDates)
43
044
=== added file 'GTG/tests/test_syncengine.py'
--- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncengine.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,189 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the SyncEngine class
22'''
23
24import unittest
25import uuid
26
27from GTG.backends.syncengine import SyncEngine
28
29
30
31class TestSyncEngine(unittest.TestCase):
32 '''
33 Tests for the SyncEngine object.
34 '''
35
36 def setUp(self):
37 self.ftp_local = FakeTaskProvider()
38 self.ftp_remote = FakeTaskProvider()
39 self.sync_engine = SyncEngine()
40
41 def test_analyze_element_and_record_and_break_relationship(self):
42 '''
43 Test for the _analyze_element, analyze_remote_id, analyze_local_id,
44 record_relationship, break_relationship
45 '''
46 #adding a new local task
47 local_id = uuid.uuid4()
48 self.ftp_local.fake_add_task(local_id)
49 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
50 self.ftp_local.has_task, self.ftp_remote.has_task), \
51 (SyncEngine.ADD, None))
52 #creating the related remote task
53 remote_id = uuid.uuid4()
54 self.ftp_remote.fake_add_task(remote_id)
55 #informing the sync_engine about that
56 self.sync_engine.record_relationship(local_id, remote_id, object())
57 #verifying that it understood that
58 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
59 self.ftp_local.has_task, self.ftp_remote.has_task), \
60 (SyncEngine.UPDATE, remote_id))
61 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
62 self.ftp_local.has_task, self.ftp_remote.has_task), \
63 (SyncEngine.UPDATE, local_id))
64 #and not the reverse
65 self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \
66 self.ftp_local.has_task, self.ftp_remote.has_task), \
67 (SyncEngine.ADD, None))
68 self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \
69 self.ftp_local.has_task, self.ftp_remote.has_task), \
70 (SyncEngine.ADD, None))
71 #now we remove the remote task
72 self.ftp_remote.fake_remove_task(remote_id)
73 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
74 self.ftp_local.has_task, self.ftp_remote.has_task), \
75 (SyncEngine.REMOVE, None))
76 self.sync_engine.break_relationship(local_id = local_id)
77 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
78 self.ftp_local.has_task, self.ftp_remote.has_task), \
79 (SyncEngine.ADD, None))
80 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
81 self.ftp_local.has_task, self.ftp_remote.has_task), \
82 (SyncEngine.ADD, None))
83 #we add them back and remove giving the remote id as key to find what to
84 #delete
85 self.ftp_local.fake_add_task(local_id)
86 self.ftp_remote.fake_add_task(remote_id)
87 self.ftp_remote.fake_remove_task(remote_id)
88 self.sync_engine.record_relationship(local_id, remote_id, object)
89 self.sync_engine.break_relationship(remote_id = remote_id)
90 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
91 self.ftp_local.has_task, self.ftp_remote.has_task), \
92 (SyncEngine.ADD, None))
93 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
94 self.ftp_local.has_task, self.ftp_remote.has_task), \
95 (SyncEngine.ADD, None))
96
97 def test_syncability(self):
98 '''
99 Test for the _analyze_element, analyze_remote_id, analyze_local_id.
100 Checks that the is_syncable parameter is used correctly
101 '''
102 #adding a new local task unsyncable
103 local_id = uuid.uuid4()
104 self.ftp_local.fake_add_task(local_id)
105 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
106 self.ftp_local.has_task, self.ftp_remote.has_task,
107 False), \
108 (None, None))
109 #adding a new local task, syncable
110 local_id = uuid.uuid4()
111 self.ftp_local.fake_add_task(local_id)
112 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
113 self.ftp_local.has_task, self.ftp_remote.has_task), \
114 (SyncEngine.ADD, None))
115 #creating the related remote task
116 remote_id = uuid.uuid4()
117 self.ftp_remote.fake_add_task(remote_id)
118 #informing the sync_engine about that
119 self.sync_engine.record_relationship(local_id, remote_id, object())
120 #checking that it behaves correctly with established relationships
121 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
122 self.ftp_local.has_task, self.ftp_remote.has_task,
123 True), \
124 (SyncEngine.UPDATE, remote_id))
125 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
126 self.ftp_local.has_task, self.ftp_remote.has_task,
127 False), \
128 (SyncEngine.LOST_SYNCABILITY, remote_id))
129 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
130 self.ftp_local.has_task, self.ftp_remote.has_task,
131 True), \
132 (SyncEngine.UPDATE, local_id))
133 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
134 self.ftp_local.has_task, self.ftp_remote.has_task,
135 False), \
136 (SyncEngine.LOST_SYNCABILITY, local_id))
137 #now we remove the remote task
138 self.ftp_remote.fake_remove_task(remote_id)
139 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
140 self.ftp_local.has_task, self.ftp_remote.has_task,
141 True), \
142 (SyncEngine.REMOVE, None))
143 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
144 self.ftp_local.has_task, self.ftp_remote.has_task,
145 False), \
146 (SyncEngine.REMOVE, None))
147 self.sync_engine.break_relationship(local_id = local_id)
148 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
149 self.ftp_local.has_task, self.ftp_remote.has_task,
150 True), \
151 (SyncEngine.ADD, None))
152 self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
153 self.ftp_local.has_task, self.ftp_remote.has_task,
154 False), \
155 (None, None))
156 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
157 self.ftp_local.has_task, self.ftp_remote.has_task,
158 True), \
159 (SyncEngine.ADD, None))
160 self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
161 self.ftp_local.has_task, self.ftp_remote.has_task,
162 False), \
163 (None, None))
164
165def test_suite():
166 return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine)
167
168
169class FakeTaskProvider(object):
170
171 def __init__(self):
172 self.dic = {}
173
174 def has_task(self, tid):
175 return self.dic.has_key(tid)
176
177###############################################################################
178### Function with the fake_ prefix are here to assist in testing, they do not
179### need to be present in the real class
180###############################################################################
181
182 def fake_add_task(self, tid):
183 self.dic[tid] = "something"
184
185 def fake_get_task(self, tid):
186 return self.dic[tid]
187
188 def fake_remove_task(self, tid):
189 del self.dic[tid]
0190
=== added file 'GTG/tests/test_syncmeme.py'
--- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncmeme.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,59 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the SyncMeme class
22'''
23
24import unittest
25import datetime
26
27from GTG.backends.syncengine import SyncMeme
28
29
30
31class TestSyncMeme(unittest.TestCase):
32 '''
33 Tests for the SyncEngine object.
34 '''
35
36 def test_which_is_newest(self):
37 '''
38 test the which_is_newest function
39
40 '''
41 meme = SyncMeme()
42 #tasks have not changed
43 local_modified = datetime.datetime.now()
44 remote_modified = datetime.datetime.now()
45 meme.set_local_last_modified(local_modified)
46 meme.set_remote_last_modified(remote_modified)
47 self.assertEqual(meme.which_is_newest(local_modified, \
48 remote_modified), None)
49 #we update the local
50 local_modified = datetime.datetime.now()
51 self.assertEqual(meme.which_is_newest(local_modified, \
52 remote_modified), 'local')
53 #we update the remote
54 remote_modified = datetime.datetime.now()
55 self.assertEqual(meme.which_is_newest(local_modified, \
56 remote_modified), 'remote')
57def test_suite():
58 return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme)
59
060
=== added file 'GTG/tests/test_twokeydict.py'
--- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_twokeydict.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,98 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the TwoKeyDict class
22'''
23
24import unittest
25import uuid
26
27from GTG.tools.twokeydict import TwoKeyDict
28
29
30
31class TestTwoKeyDict(unittest.TestCase):
32 '''
33 Tests for the TwoKeyDict object.
34 '''
35
36
37 def test_add_and_gets(self):
38 '''
39 Test for the __init__, _get_by_first, _get_by_second function
40 '''
41 triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
42 for a in xrange(10)]
43 tw_dict = TwoKeyDict(*triplets)
44 for triplet in triplets:
45 self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2])
46 self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2])
47
48 def test_remove_by_first_or_second(self):
49 '''
50 Test for removing triplets form the TwoKeyDict
51 '''
52 triplet_first = (1, 'I', 'one')
53 triplet_second = (2, 'II', 'two')
54 tw_dict = TwoKeyDict(triplet_first, triplet_second)
55 tw_dict._remove_by_primary(triplet_first[0])
56 tw_dict._remove_by_secondary(triplet_second[1])
57 missing_first = 0
58 missing_second = 0
59 try:
60 tw_dict._get_by_primary(triplet_first[0])
61 except KeyError:
62 missing_first += 1
63 try:
64 tw_dict._get_by_secondary(triplet_second[0])
65 except KeyError:
66 missing_first += 1
67 try:
68 tw_dict._get_by_secondary(triplet_first[1])
69 except KeyError:
70 missing_second += 1
71 try:
72 tw_dict._get_by_secondary(triplet_second[1])
73 except KeyError:
74 missing_second += 1
75 self.assertEqual(missing_first, 2)
76 self.assertEqual(missing_second, 2)
77 #check for memory leaks
78 dict_len = 0
79 for key in tw_dict._primary_to_value.iterkeys():
80 dict_len += 1
81 self.assertEqual(dict_len, 0)
82
83 def test_get_primary_and_secondary_key(self):
84 '''
85 Test for fetching the objects stored in the TwoKeyDict
86 '''
87 triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
88 for a in xrange(10)]
89 tw_dict = TwoKeyDict(*triplets)
90 for triplet in triplets:
91 self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \
92 triplet[1])
93 self.assertEqual(tw_dict._get_primary_key(triplet[1]), \
94 triplet[0])
95
96def test_suite():
97 return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict)
98
099
=== added file 'GTG/tools/bidict.py'
--- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/bidict.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,112 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20
21
22class BiDict(object):
23 '''
24 Bidirectional dictionary: the pairs stored can be accessed using either the
25 first or the second element as key (named key1 and key2).
26 You don't need this if there is no clash between the domains of the first
27 and second element of the pairs.
28 '''
29
30 def __init__(self, *pairs):
31 '''
32 Initialization of the bidirectional dictionary
33
34 @param pairs: optional. A list of pairs to add to the dictionary
35 '''
36 super(BiDict, self).__init__()
37 self._first_to_second = {}
38 self._second_to_first = {}
39 for pair in pairs:
40 self.add(pair)
41
42 def add(self, pair):
43 '''
44 Adds a pair (key1, key2) to the dictionary
45
46 @param pair: the pair formatted as (key1, key2)
47 '''
48 self._first_to_second[pair[0]] = pair[1]
49 self._second_to_first[pair[1]] = pair[0]
50
51 def _get_by_first(self, key):
52 '''
53 Gets the key2 given key1
54
55 @param key: the first key
56 '''
57 return self._first_to_second[key]
58
59 def _get_by_second(self, key):
60 '''
61 Gets the key1 given key2
62
63 @param key: the second key
64 '''
65 return self._second_to_first[key]
66
67 def _remove_by_first(self, first):
68 '''
69 Removes a pair given the first key
70
71 @param key: the first key
72 '''
73 second = self._first_to_second[first]
74 del self._second_to_first[second]
75 del self._first_to_second[first]
76
77 def _remove_by_second(self, second):
78 '''
79 Removes a pair given the second key
80
81 @param key: the second key
82 '''
83 first = self._second_to_first[second]
84 del self._first_to_second[first]
85 del self._second_to_first[second]
86
87 def _get_all_first(self):
88 '''
89 Returns the list of all first keys
90
91 @returns list
92 '''
93 return list(self._first_to_second)
94
95 def _get_all_second(self):
96 '''
97 Returns the list of all second keys
98
99 @returns list
100 '''
101 return list(self._second_to_first)
102
103 def __str__(self):
104 '''
105 returns a string representing the content of this BiDict
106
107 @returns string
108 '''
109 return reduce(lambda text, keys: \
110 str(text) + str(keys),
111 self._first_to_second.iteritems())
112
0113
=== added file 'GTG/tools/twokeydict.py'
--- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/twokeydict.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,135 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Contains TwoKeyDict, a Dictionary which also has a secondary key
22'''
23
24from GTG.tools.bidict import BiDict
25
26
27
28class TwoKeyDict(object):
29 '''
30 It's a standard Dictionary with a secondary key.
31 For example, you can add an element ('2', 'II', two'), where the
32 first two arguments are keys and the third is the stored object, and access
33 it as:
34 twokey['2'] ==> 'two'
35 twokey['II'] ==> 'two'
36 You can also request the other key, given one.
37 Function calls start with _ because you'll probably want to rename them when
38 you use this dictionary, for the sake of clarity.
39 '''
40
41
42 def __init__(self, *triplets):
43 '''
44 Creates the TwoKeyDict and optionally populates it with some data
45
46 @oaram triplets: tuples for populating the TwoKeyDict. Format:
47 ((key1, key2, data_to_store), ...)
48 '''
49 super(TwoKeyDict, self).__init__()
50 self._key_to_key_bidict = BiDict()
51 self._primary_to_value = {}
52 for triplet in triplets:
53 self.add(triplet)
54
55 def add(self, triplet):
56 '''
57 Adds a new triplet to the TwoKeyDict
58
59 @param triplet: a tuple formatted like this:
60 (key1, key2, data_to_store)
61 '''
62 self._key_to_key_bidict.add((triplet[0], triplet[1]))
63 self._primary_to_value[triplet[0]] = triplet[2]
64
65 def _get_by_primary(self, primary):
66 '''
67 Gets the stored data given the primary key
68
69 @param primary: the primary key
70 @returns object: the stored object
71 '''
72 return self._primary_to_value[primary]
73
74 def _get_by_secondary(self, secondary):
75 '''
76 Gets the stored data given the secondary key
77
78 @param secondary: the primary key
79 @returns object: the stored object
80 '''
81 primary = self._key_to_key_bidict._get_by_second(secondary)
82 return self._get_by_primary(primary)
83
84 def _remove_by_primary(self, primary):
85 '''
86 Removes a triplet given the rpimary key.
87
88 @param primary: the primary key
89 '''
90 del self._primary_to_value[primary]
91 self._key_to_key_bidict._remove_by_first(primary)
92
93 def _remove_by_secondary(self, secondary):
94 '''
95 Removes a triplet given the rpimary key.
96
97 @param secondary: the primary key
98 '''
99 primary = self._key_to_key_bidict._get_by_second(secondary)
100 self._remove_by_primary(primary)
101
102 def _get_secondary_key(self, primary):
103 '''
104 Gets the secondary key given the primary
105
106 @param primary: the primary key
107 @returns object: the secondary key
108 '''
109 return self._key_to_key_bidict._get_by_first(primary)
110
111 def _get_primary_key(self, secondary):
112 '''
113 Gets the primary key given the secondary
114
115 @param secondary: the secondary key
116 @returns object: the primary key
117 '''
118 return self._key_to_key_bidict._get_by_second(secondary)
119
120 def _get_all_primary_keys(self):
121 '''
122 Returns all primary keys
123
124 @returns list: list of all primary keys
125 '''
126 return self._key_to_key_bidict._get_all_first()
127
128 def _get_all_secondary_keys(self):
129 '''
130 Returns all secondary keys
131
132 @returns list: list of all secondary keys
133 '''
134 return self._key_to_key_bidict._get_all_second()
135
0136
=== added file 'GTG/tools/watchdog.py'
--- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/watchdog.py 2010-08-13 23:43:06 +0000
@@ -0,0 +1,53 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19import threading
20
21class Watchdog(object):
22 '''
23 a simple thread-safe watchdog.
24 usage:
25 with Watchdod(timeout, error_function):
26 #do something
27 '''
28
29 def __init__(self, timeout, error_function):
30 '''
31 Just sets the timeout and the function to execute when an error occours
32
33 @param timeout: timeout in seconds
34 @param error_function: what to execute in case the watchdog timer
35 triggers
36 '''
37 self.timeout = timeout
38 self.error_function = error_function
39
40 def __enter__(self):
41 '''Starts the countdown'''
42 self.timer = threading.Timer(self.timeout, self.error_function)
43 self.timer.start()
44
45 def __exit__(self, type, value, traceback):
46 '''Aborts the countdown'''
47 try:
48 self.timer.cancel()
49 except:
50 pass
51 if value == None:
52 return True
53 return False

Subscribers

People subscribed via source and target branches

to status/vote changes: