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