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