All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
14 from .backends import TaskWarrior
15 from .task import Task, ReadOnlyDictView, TaskQuerySet
16 from .lazy import LazyUUIDTask, LazyUUIDTaskSet
17 from .serializing import DATE_FORMAT, local_zone
19 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
20 TASK_STANDARD_ATTRS = (
43 total_seconds_2_6 = lambda x: x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
46 class TasklibTest(unittest.TestCase):
49 self.tmp = tempfile.mkdtemp(dir='.')
50 self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/')
53 shutil.rmtree(self.tmp)
56 class TaskFilterTest(TasklibTest):
58 def test_all_empty(self):
59 self.assertEqual(len(self.tw.tasks.all()), 0)
61 def test_all_non_empty(self):
62 Task(self.tw, description="test task").save()
63 self.assertEqual(len(self.tw.tasks.all()), 1)
64 self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
65 self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
67 def test_pending_non_empty(self):
68 Task(self.tw, description="test task").save()
69 self.assertEqual(len(self.tw.tasks.pending()), 1)
70 self.assertEqual(self.tw.tasks.pending()[0]['description'],
72 self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
74 def test_completed_empty(self):
75 Task(self.tw, description="test task").save()
76 self.assertEqual(len(self.tw.tasks.completed()), 0)
78 def test_completed_non_empty(self):
79 Task(self.tw, description="test task").save()
80 self.assertEqual(len(self.tw.tasks.completed()), 0)
81 self.tw.tasks.all()[0].done()
82 self.assertEqual(len(self.tw.tasks.completed()), 1)
84 def test_deleted_empty(self):
85 Task(self.tw, description="test task").save()
86 self.assertEqual(len(self.tw.tasks.deleted()), 0)
88 def test_deleted_non_empty(self):
89 Task(self.tw, description="test task").save()
90 self.assertEqual(len(self.tw.tasks.deleted()), 0)
91 self.tw.tasks.all()[0].delete()
92 self.assertEqual(len(self.tw.tasks.deleted()), 1)
94 def test_waiting_empty(self):
95 Task(self.tw, description="test task").save()
96 self.assertEqual(len(self.tw.tasks.waiting()), 0)
98 def test_waiting_non_empty(self):
99 Task(self.tw, description="test task").save()
100 self.assertEqual(len(self.tw.tasks.waiting()), 0)
102 t = self.tw.tasks.all()[0]
103 t['wait'] = 'tomorrow'
106 self.assertEqual(len(self.tw.tasks.waiting()), 1)
108 def test_filtering_by_attribute(self):
109 Task(self.tw, description="no priority task").save()
110 Task(self.tw, priority="H", description="high priority task").save()
111 self.assertEqual(len(self.tw.tasks.all()), 2)
113 # Assert that the correct number of tasks is returned
114 self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
116 # Assert that the correct tasks are returned
117 high_priority_task = self.tw.tasks.get(priority="H")
118 self.assertEqual(high_priority_task['description'], "high priority task")
120 def test_filtering_by_empty_attribute(self):
121 Task(self.tw, description="no priority task").save()
122 Task(self.tw, priority="H", description="high priority task").save()
123 self.assertEqual(len(self.tw.tasks.all()), 2)
125 # Assert that the correct number of tasks is returned
126 self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
128 # Assert that the correct tasks are returned
129 no_priority_task = self.tw.tasks.get(priority=None)
130 self.assertEqual(no_priority_task['description'], "no priority task")
132 def test_filter_for_task_with_space_in_descripition(self):
133 task = Task(self.tw, description="test task")
136 filtered_task = self.tw.tasks.get(description="test task")
137 self.assertEqual(filtered_task['description'], "test task")
139 def test_filter_for_task_without_space_in_descripition(self):
140 task = Task(self.tw, description="test")
143 filtered_task = self.tw.tasks.get(description="test")
144 self.assertEqual(filtered_task['description'], "test")
146 def test_filter_for_task_with_space_in_project(self):
147 task = Task(self.tw, description="test", project="random project")
150 filtered_task = self.tw.tasks.get(project="random project")
151 self.assertEqual(filtered_task['project'], "random project")
153 def test_filter_for_task_without_space_in_project(self):
154 task = Task(self.tw, description="test", project="random")
157 filtered_task = self.tw.tasks.get(project="random")
158 self.assertEqual(filtered_task['project'], "random")
160 def test_filter_with_empty_uuid(self):
161 self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
163 def test_filter_dummy_by_status(self):
164 t = Task(self.tw, description="test")
167 tasks = self.tw.tasks.filter(status=t['status'])
168 self.assertEqual(list(tasks), [t])
170 def test_filter_dummy_by_uuid(self):
171 t = Task(self.tw, description="test")
174 tasks = self.tw.tasks.filter(uuid=t['uuid'])
175 self.assertEqual(list(tasks), [t])
177 def test_filter_dummy_by_entry(self):
178 t = Task(self.tw, description="test")
181 tasks = self.tw.tasks.filter(entry=t['entry'])
182 self.assertEqual(list(tasks), [t])
184 def test_filter_dummy_by_description(self):
185 t = Task(self.tw, description="test")
188 tasks = self.tw.tasks.filter(description=t['description'])
189 self.assertEqual(list(tasks), [t])
191 def test_filter_dummy_by_start(self):
192 t = Task(self.tw, description="test")
196 tasks = self.tw.tasks.filter(start=t['start'])
197 self.assertEqual(list(tasks), [t])
199 def test_filter_dummy_by_end(self):
200 t = Task(self.tw, description="test")
204 tasks = self.tw.tasks.filter(end=t['end'])
205 self.assertEqual(list(tasks), [t])
207 def test_filter_dummy_by_due(self):
208 t = Task(self.tw, description="test", due=datetime.datetime.now())
211 tasks = self.tw.tasks.filter(due=t['due'])
212 self.assertEqual(list(tasks), [t])
214 def test_filter_dummy_by_until(self):
215 t = Task(self.tw, description="test")
218 tasks = self.tw.tasks.filter(until=t['until'])
219 self.assertEqual(list(tasks), [t])
221 def test_filter_dummy_by_modified(self):
222 # Older TW version does not support bumping modified
224 if self.tw.version < six.text_type('2.2.0'):
225 # Python2.6 does not support SkipTest. As a workaround
226 # mark the test as passed by exiting.
227 if getattr(unittest, 'SkipTest', None) is not None:
228 raise unittest.SkipTest()
232 t = Task(self.tw, description="test")
235 tasks = self.tw.tasks.filter(modified=t['modified'])
236 self.assertEqual(list(tasks), [t])
238 def test_filter_dummy_by_scheduled(self):
239 t = Task(self.tw, description="test")
242 tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
243 self.assertEqual(list(tasks), [t])
245 def test_filter_dummy_by_tags(self):
246 t = Task(self.tw, description="test", tags=["home"])
249 tasks = self.tw.tasks.filter(tags=t['tags'])
250 self.assertEqual(list(tasks), [t])
252 def test_filter_dummy_by_projects(self):
253 t = Task(self.tw, description="test", project="random")
256 tasks = self.tw.tasks.filter(project=t['project'])
257 self.assertEqual(list(tasks), [t])
259 def test_filter_by_priority(self):
260 t = Task(self.tw, description="test", priority="H")
263 tasks = self.tw.tasks.filter(priority=t['priority'])
264 self.assertEqual(list(tasks), [t])
267 class TaskTest(TasklibTest):
269 def test_create_unsaved_task(self):
270 # Make sure a new task is not saved unless explicitly called for
271 t = Task(self.tw, description="test task")
272 self.assertEqual(len(self.tw.tasks.all()), 0)
274 # TODO: once python 2.6 compatiblity is over, use context managers here
275 # and in all subsequent tests for assertRaises
277 def test_delete_unsaved_task(self):
278 t = Task(self.tw, description="test task")
279 self.assertRaises(Task.NotSaved, t.delete)
281 def test_complete_unsaved_task(self):
282 t = Task(self.tw, description="test task")
283 self.assertRaises(Task.NotSaved, t.done)
285 def test_refresh_unsaved_task(self):
286 t = Task(self.tw, description="test task")
287 self.assertRaises(Task.NotSaved, t.refresh)
289 def test_start_unsaved_task(self):
290 t = Task(self.tw, description="test task")
291 self.assertRaises(Task.NotSaved, t.start)
293 def test_delete_deleted_task(self):
294 t = Task(self.tw, description="test task")
298 self.assertRaises(Task.DeletedTask, t.delete)
300 def test_complete_completed_task(self):
301 t = Task(self.tw, description="test task")
305 self.assertRaises(Task.CompletedTask, t.done)
307 def test_start_completed_task(self):
308 t = Task(self.tw, description="test task")
312 self.assertRaises(Task.CompletedTask, t.start)
314 def test_add_completed_task(self):
315 t = Task(self.tw, description="test", status="completed",
316 end=datetime.datetime.now())
319 def test_add_multiple_completed_tasks(self):
320 t1 = Task(self.tw, description="test1", status="completed",
321 end=datetime.datetime.now())
322 t2 = Task(self.tw, description="test2", status="completed",
323 end=datetime.datetime.now())
327 def test_complete_deleted_task(self):
328 t = Task(self.tw, description="test task")
332 self.assertRaises(Task.DeletedTask, t.done)
334 def test_starting_task(self):
335 t = Task(self.tw, description="test task")
336 now = t.datetime_normalizer(datetime.datetime.now())
340 self.assertTrue(now.replace(microsecond=0) <= t['start'])
341 self.assertEqual(t['status'], 'pending')
343 def test_completing_task(self):
344 t = Task(self.tw, description="test task")
345 now = t.datetime_normalizer(datetime.datetime.now())
349 self.assertTrue(now.replace(microsecond=0) <= t['end'])
350 self.assertEqual(t['status'], 'completed')
352 def test_deleting_task(self):
353 t = Task(self.tw, description="test task")
354 now = t.datetime_normalizer(datetime.datetime.now())
358 self.assertTrue(now.replace(microsecond=0) <= t['end'])
359 self.assertEqual(t['status'], 'deleted')
361 def test_started_task_active(self):
362 t = Task(self.tw, description="test task")
365 self.assertTrue(t.active)
367 def test_unstarted_task_inactive(self):
368 t = Task(self.tw, description="test task")
369 self.assertFalse(t.active)
371 self.assertFalse(t.active)
373 def test_start_active_task(self):
374 t = Task(self.tw, description="test task")
377 self.assertRaises(Task.ActiveTask, t.start)
379 def test_stop_completed_task(self):
380 t = Task(self.tw, description="test task")
385 self.assertRaises(Task.InactiveTask, t.stop)
387 t = Task(self.tw, description="test task")
391 self.assertRaises(Task.InactiveTask, t.stop)
393 def test_stop_deleted_task(self):
394 t = Task(self.tw, description="test task")
400 def test_stop_inactive_task(self):
401 t = Task(self.tw, description="test task")
404 self.assertRaises(Task.InactiveTask, t.stop)
406 t = Task(self.tw, description="test task")
411 self.assertRaises(Task.InactiveTask, t.stop)
413 def test_stopping_task(self):
414 t = Task(self.tw, description="test task")
415 now = t.datetime_normalizer(datetime.datetime.now())
420 self.assertEqual(t['end'], None)
421 self.assertEqual(t['status'], 'pending')
422 self.assertFalse(t.active)
424 def test_modify_simple_attribute_without_space(self):
425 t = Task(self.tw, description="test")
428 self.assertEquals(t['description'], "test")
430 t['description'] = "test-modified"
433 self.assertEquals(t['description'], "test-modified")
435 def test_modify_simple_attribute_with_space(self):
436 # Space can pose problems with parsing
437 t = Task(self.tw, description="test task")
440 self.assertEquals(t['description'], "test task")
442 t['description'] = "test task modified"
445 self.assertEquals(t['description'], "test task modified")
447 def test_empty_dependency_set_of_unsaved_task(self):
448 t = Task(self.tw, description="test task")
449 self.assertEqual(t['depends'], set())
451 def test_empty_dependency_set_of_saved_task(self):
452 t = Task(self.tw, description="test task")
454 self.assertEqual(t['depends'], set())
456 def test_set_unsaved_task_as_dependency(self):
457 # Adds only one dependency to task with no dependencies
458 t = Task(self.tw, description="test task")
459 dependency = Task(self.tw, description="needs to be done first")
461 # We only save the parent task, dependency task is unsaved
463 t['depends'] = set([dependency])
465 self.assertRaises(Task.NotSaved, t.save)
467 def test_set_simple_dependency_set(self):
468 # Adds only one dependency to task with no dependencies
469 t = Task(self.tw, description="test task")
470 dependency = Task(self.tw, description="needs to be done first")
475 t['depends'] = set([dependency])
477 self.assertEqual(t['depends'], set([dependency]))
479 def test_set_complex_dependency_set(self):
480 # Adds two dependencies to task with no dependencies
481 t = Task(self.tw, description="test task")
482 dependency1 = Task(self.tw, description="needs to be done first")
483 dependency2 = Task(self.tw, description="needs to be done second")
489 t['depends'] = set([dependency1, dependency2])
491 self.assertEqual(t['depends'], set([dependency1, dependency2]))
493 def test_remove_from_dependency_set(self):
494 # Removes dependency from task with two dependencies
495 t = Task(self.tw, description="test task")
496 dependency1 = Task(self.tw, description="needs to be done first")
497 dependency2 = Task(self.tw, description="needs to be done second")
502 t['depends'] = set([dependency1, dependency2])
505 t['depends'].remove(dependency2)
508 self.assertEqual(t['depends'], set([dependency1]))
510 def test_add_to_dependency_set(self):
511 # Adds dependency to task with one dependencies
512 t = Task(self.tw, description="test task")
513 dependency1 = Task(self.tw, description="needs to be done first")
514 dependency2 = Task(self.tw, description="needs to be done second")
519 t['depends'] = set([dependency1])
522 t['depends'].add(dependency2)
525 self.assertEqual(t['depends'], set([dependency1, dependency2]))
527 def test_add_to_empty_dependency_set(self):
528 # Adds dependency to task with one dependencies
529 t = Task(self.tw, description="test task")
530 dependency = Task(self.tw, description="needs to be done first")
534 t['depends'].add(dependency)
537 self.assertEqual(t['depends'], set([dependency]))
539 def test_simple_dependency_set_save_repeatedly(self):
540 # Adds only one dependency to task with no dependencies
541 t = Task(self.tw, description="test task")
542 dependency = Task(self.tw, description="needs to be done first")
545 t['depends'] = set([dependency])
548 # We taint the task, but keep depends intact
549 t['description'] = "test task modified"
552 self.assertEqual(t['depends'], set([dependency]))
554 # We taint the task, but assign the same set to the depends
555 t['depends'] = set([dependency])
556 t['description'] = "test task modified again"
559 self.assertEqual(t['depends'], set([dependency]))
561 def test_compare_different_tasks(self):
562 # Negative: compare two different tasks
563 t1 = Task(self.tw, description="test task")
564 t2 = Task(self.tw, description="test task")
569 self.assertEqual(t1 == t2, False)
571 def test_compare_same_task_object(self):
572 # Compare Task object wit itself
573 t = Task(self.tw, description="test task")
576 self.assertEqual(t == t, True)
578 def test_compare_same_task(self):
579 # Compare the same task using two different objects
580 t1 = Task(self.tw, description="test task")
583 t2 = self.tw.tasks.get(uuid=t1['uuid'])
584 self.assertEqual(t1 == t2, True)
586 def test_compare_unsaved_tasks(self):
587 # t1 and t2 are unsaved tasks, considered to be unequal
588 # despite the content of data
589 t1 = Task(self.tw, description="test task")
590 t2 = Task(self.tw, description="test task")
592 self.assertEqual(t1 == t2, False)
594 def test_hash_unsaved_tasks(self):
595 # Considered equal, it's the same object
596 t1 = Task(self.tw, description="test task")
598 self.assertEqual(hash(t1) == hash(t2), True)
600 def test_hash_same_task(self):
601 # Compare the hash of the task using two different objects
602 t1 = Task(self.tw, description="test task")
605 t2 = self.tw.tasks.get(uuid=t1['uuid'])
606 self.assertEqual(t1.__hash__(), t2.__hash__())
608 def test_adding_task_with_priority(self):
609 t = Task(self.tw, description="test task", priority="M")
612 def test_removing_priority_with_none(self):
613 t = Task(self.tw, description="test task", priority="L")
616 # Remove the priority mark
620 # Assert that priority is not there after saving
621 self.assertEqual(t['priority'], None)
623 def test_adding_task_with_due_time(self):
624 t = Task(self.tw, description="test task", due=datetime.datetime.now())
627 def test_removing_due_time_with_none(self):
628 t = Task(self.tw, description="test task", due=datetime.datetime.now())
631 # Remove the due timestamp
635 # Assert that due timestamp is no longer there
636 self.assertEqual(t['due'], None)
638 def test_modified_fields_new_task(self):
641 # This should be empty with new task
642 self.assertEqual(set(t._modified_fields), set())
645 t['description'] = "test task"
646 self.assertEqual(set(t._modified_fields), set(['description']))
648 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
649 self.assertEqual(set(t._modified_fields), set(['description', 'due']))
651 t['project'] = "test project"
652 self.assertEqual(set(t._modified_fields),
653 set(['description', 'due', 'project']))
655 # List of modified fields should clear out when saved
657 self.assertEqual(set(t._modified_fields), set())
659 # Reassigning the fields with the same values now should not produce
661 t['description'] = "test task"
662 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
663 t['project'] = "test project"
664 self.assertEqual(set(t._modified_fields), set())
666 def test_modified_fields_loaded_task(self):
670 t['description'] = "test task"
671 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
672 t['project'] = "test project"
674 dependency = Task(self.tw, description="dependency")
676 t['depends'] = set([dependency])
678 # List of modified fields should clear out when saved
680 self.assertEqual(set(t._modified_fields), set())
682 # Get the task by using a filter by UUID
683 t2 = self.tw.tasks.get(uuid=t['uuid'])
685 # Reassigning the fields with the same values now should not produce
687 t['description'] = "test task"
688 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
689 t['project'] = "test project"
690 t['depends'] = set([dependency])
691 self.assertEqual(set(t._modified_fields), set())
693 def test_modified_fields_not_affected_by_reading(self):
696 for field in TASK_STANDARD_ATTRS:
699 self.assertEqual(set(t._modified_fields), set())
701 def test_setting_read_only_attrs_through_init(self):
702 # Test that we are unable to set readonly attrs through __init__
703 for readonly_key in Task.read_only_fields:
704 kwargs = {'description': 'test task', readonly_key: 'value'}
705 self.assertRaises(RuntimeError,
706 lambda: Task(self.tw, **kwargs))
708 def test_setting_read_only_attrs_through_setitem(self):
709 # Test that we are unable to set readonly attrs through __init__
710 for readonly_key in Task.read_only_fields:
711 t = Task(self.tw, description='test task')
712 self.assertRaises(RuntimeError,
713 lambda: t.__setitem__(readonly_key, 'value'))
715 def test_saving_unmodified_task(self):
716 t = Task(self.tw, description="test task")
720 def test_adding_tag_by_appending(self):
721 t = Task(self.tw, description="test task", tags=['test1'])
723 t['tags'].add('test2')
725 self.assertEqual(t['tags'], set(['test1', 'test2']))
727 def test_adding_tag_twice(self):
728 t = Task(self.tw, description="test task", tags=['test1'])
730 t['tags'].add('test2')
731 t['tags'].add('test2')
733 self.assertEqual(t['tags'], set(['test1', 'test2']))
735 def test_adding_tag_by_appending_empty(self):
736 t = Task(self.tw, description="test task")
738 t['tags'].add('test')
740 self.assertEqual(t['tags'], set(['test']))
742 def test_serializers_returning_empty_string_for_none(self):
743 # Test that any serializer returns '' when passed None
745 serializers = [getattr(t, serializer_name) for serializer_name in
746 filter(lambda x: x.startswith('serialize_'), dir(t))]
747 for serializer in serializers:
748 self.assertEqual(serializer(None), '')
750 def test_deserializer_returning_empty_value_for_empty_string(self):
751 # Test that any deserializer returns empty value when passed ''
753 deserializers = [getattr(t, deserializer_name) for deserializer_name in
754 filter(lambda x: x.startswith('deserialize_'), dir(t))]
755 for deserializer in deserializers:
756 self.assertTrue(deserializer('') in (None, [], set()))
758 def test_normalizers_handling_none(self):
759 # Test that any normalizer can handle None as a valid value
762 for key in TASK_STANDARD_ATTRS:
763 t._normalize(key, None)
765 def test_recurrent_task_generation(self):
766 today = datetime.date.today()
767 t = Task(self.tw, description="brush teeth",
768 due=today, recur="daily")
770 self.assertEqual(len(self.tw.tasks.pending()), 2)
772 def test_spawned_task_parent(self):
773 today = datetime.date.today()
774 t = Task(self.tw, description="brush teeth",
775 due=today, recur="daily")
778 spawned = self.tw.tasks.pending().get(due=today)
779 assert spawned['parent'] == t
781 def test_modify_number_of_tasks_at_once(self):
782 for i in range(1, 100):
783 Task(self.tw, description="test task %d" % i, tags=['test']).save()
785 self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
787 def test_return_all_from_executed_command(self):
788 Task(self.tw, description="test task", tags=['test']).save()
789 out, err, rc = self.tw.execute_command(['count'], return_all=True)
790 self.assertEqual(rc, 0)
792 def test_return_all_from_failed_executed_command(self):
793 Task(self.tw, description="test task", tags=['test']).save()
794 out, err, rc = self.tw.execute_command(['countinvalid'],
795 return_all=True, allow_failure=False)
796 self.assertNotEqual(rc, 0)
799 class TaskFromHookTest(TasklibTest):
801 input_add_data = six.StringIO(
802 '{"description":"Buy some milk",'
803 '"entry":"20141118T050231Z",'
804 '"status":"pending",'
805 '"start":"20141119T152233Z",'
806 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
808 input_add_data_recurring = six.StringIO(
809 '{"description":"Mow the lawn",'
810 '"entry":"20160210T224304Z",'
811 '"parent":"62da6227-519c-42c2-915d-dccada926ad7",'
813 '"status":"pending",'
814 '"uuid":"81305335-0237-49ff-8e87-b3cdc2369cec"}')
816 input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
817 '{"description":"Buy some milk finally",'
818 '"entry":"20141118T050231Z",'
819 '"status":"completed",'
820 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
822 exported_raw_data = (
824 '"due":"20150101T232323Z",'
825 '"description":"test task"}')
827 def test_setting_up_from_add_hook_input(self):
828 t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
829 self.assertEqual(t['description'], "Buy some milk")
830 self.assertEqual(t.pending, True)
832 def test_setting_up_from_add_hook_input_recurring(self):
833 t = Task.from_input(input_file=self.input_add_data_recurring,
835 self.assertEqual(t['description'], "Mow the lawn")
836 self.assertEqual(t.pending, True)
838 def test_setting_up_from_modified_hook_input(self):
839 t = Task.from_input(input_file=self.input_modify_data, modify=True,
841 self.assertEqual(t['description'], "Buy some milk finally")
842 self.assertEqual(t.pending, False)
843 self.assertEqual(t.completed, True)
845 self.assertEqual(t._original_data['status'], "pending")
846 self.assertEqual(t._original_data['description'], "Buy some milk")
847 self.assertEqual(set(t._modified_fields),
848 set(['status', 'description', 'start']))
850 def test_export_data(self):
851 t = Task(self.tw, description="test task",
853 due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
855 # Check that the output is a permutation of:
856 # {"project":"Home","description":"test task","due":"20150101232323Z"}
857 allowed_segments = self.exported_raw_data[1:-1].split(',')
859 '{' + ','.join(segments) + '}'
860 for segments in itertools.permutations(allowed_segments)
863 self.assertTrue(any(t.export_data() == expected
864 for expected in allowed_output))
866 class TimezoneAwareDatetimeTest(TasklibTest):
869 super(TimezoneAwareDatetimeTest, self).setUp()
870 self.zone = local_zone
871 self.localdate_naive = datetime.datetime(2015,2,2)
872 self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
873 self.localtime_aware = self.zone.localize(self.localtime_naive)
874 self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
876 def test_timezone_naive_datetime_setitem(self):
877 t = Task(self.tw, description="test task")
878 t['due'] = self.localtime_naive
879 self.assertEqual(t['due'], self.localtime_aware)
881 def test_timezone_naive_datetime_using_init(self):
882 t = Task(self.tw, description="test task", due=self.localtime_naive)
883 self.assertEqual(t['due'], self.localtime_aware)
885 def test_filter_by_naive_datetime(self):
886 t = Task(self.tw, description="task1", due=self.localtime_naive)
888 matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
889 self.assertEqual(len(matching_tasks), 1)
891 def test_serialize_naive_datetime(self):
892 t = Task(self.tw, description="task1", due=self.localtime_naive)
893 self.assertEqual(json.loads(t.export_data())['due'],
894 self.utctime_aware.strftime(DATE_FORMAT))
896 def test_timezone_naive_date_setitem(self):
897 t = Task(self.tw, description="test task")
898 t['due'] = self.localdate_naive
899 self.assertEqual(t['due'], self.localtime_aware)
901 def test_timezone_naive_date_using_init(self):
902 t = Task(self.tw, description="test task", due=self.localdate_naive)
903 self.assertEqual(t['due'], self.localtime_aware)
905 def test_filter_by_naive_date(self):
906 t = Task(self.tw, description="task1", due=self.localdate_naive)
908 matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
909 self.assertEqual(len(matching_tasks), 1)
911 def test_serialize_naive_date(self):
912 t = Task(self.tw, description="task1", due=self.localdate_naive)
913 self.assertEqual(json.loads(t.export_data())['due'],
914 self.utctime_aware.strftime(DATE_FORMAT))
916 def test_timezone_aware_datetime_setitem(self):
917 t = Task(self.tw, description="test task")
918 t['due'] = self.localtime_aware
919 self.assertEqual(t['due'], self.localtime_aware)
921 def test_timezone_aware_datetime_using_init(self):
922 t = Task(self.tw, description="test task", due=self.localtime_aware)
923 self.assertEqual(t['due'], self.localtime_aware)
925 def test_filter_by_aware_datetime(self):
926 t = Task(self.tw, description="task1", due=self.localtime_aware)
928 matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
929 self.assertEqual(len(matching_tasks), 1)
931 def test_serialize_aware_datetime(self):
932 t = Task(self.tw, description="task1", due=self.localtime_aware)
933 self.assertEqual(json.loads(t.export_data())['due'],
934 self.utctime_aware.strftime(DATE_FORMAT))
936 class DatetimeStringTest(TasklibTest):
938 def test_simple_now_conversion(self):
939 if self.tw.version < six.text_type('2.4.0'):
940 # Python2.6 does not support SkipTest. As a workaround
941 # mark the test as passed by exiting.
942 if getattr(unittest, 'SkipTest', None) is not None:
943 raise unittest.SkipTest()
947 t = Task(self.tw, description="test task", due="now")
948 now = local_zone.localize(datetime.datetime.now())
950 # Assert that both times are not more than 5 seconds apart
951 if sys.version_info < (2,7):
952 self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
953 self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
955 self.assertTrue((now - t['due']).total_seconds() < 5)
956 self.assertTrue((t['due'] - now).total_seconds() < 5)
958 def test_simple_eoy_conversion(self):
959 if self.tw.version < six.text_type('2.4.0'):
960 # Python2.6 does not support SkipTest. As a workaround
961 # mark the test as passed by exiting.
962 if getattr(unittest, 'SkipTest', None) is not None:
963 raise unittest.SkipTest()
967 t = Task(self.tw, description="test task", due="eoy")
968 now = local_zone.localize(datetime.datetime.now())
969 eoy = local_zone.localize(datetime.datetime(
977 self.assertEqual(eoy, t['due'])
979 def test_complex_eoy_conversion(self):
980 if self.tw.version < six.text_type('2.4.0'):
981 # Python2.6 does not support SkipTest. As a workaround
982 # mark the test as passed by exiting.
983 if getattr(unittest, 'SkipTest', None) is not None:
984 raise unittest.SkipTest()
988 t = Task(self.tw, description="test task", due="eoy - 4 months")
989 now = local_zone.localize(datetime.datetime.now())
990 due_date = local_zone.localize(datetime.datetime(
997 )) - datetime.timedelta(0,4 * 30 * 86400)
998 self.assertEqual(due_date, t['due'])
1000 def test_filtering_with_string_datetime(self):
1001 if self.tw.version < six.text_type('2.4.0'):
1002 # Python2.6 does not support SkipTest. As a workaround
1003 # mark the test as passed by exiting.
1004 if getattr(unittest, 'SkipTest', None) is not None:
1005 raise unittest.SkipTest()
1009 t = Task(self.tw, description="test task",
1010 due=datetime.datetime.now() - datetime.timedelta(0,2))
1012 self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
1014 class AnnotationTest(TasklibTest):
1017 super(AnnotationTest, self).setUp()
1018 Task(self.tw, description="test task").save()
1020 def test_adding_annotation(self):
1021 task = self.tw.tasks.get()
1022 task.add_annotation('test annotation')
1023 self.assertEqual(len(task['annotations']), 1)
1024 ann = task['annotations'][0]
1025 self.assertEqual(ann['description'], 'test annotation')
1027 def test_removing_annotation(self):
1028 task = self.tw.tasks.get()
1029 task.add_annotation('test annotation')
1030 ann = task['annotations'][0]
1032 self.assertEqual(len(task['annotations']), 0)
1034 def test_removing_annotation_by_description(self):
1035 task = self.tw.tasks.get()
1036 task.add_annotation('test annotation')
1037 task.remove_annotation('test annotation')
1038 self.assertEqual(len(task['annotations']), 0)
1040 def test_removing_annotation_by_obj(self):
1041 task = self.tw.tasks.get()
1042 task.add_annotation('test annotation')
1043 ann = task['annotations'][0]
1044 task.remove_annotation(ann)
1045 self.assertEqual(len(task['annotations']), 0)
1047 def test_annotation_after_modification(self):
1048 task = self.tw.tasks.get()
1049 task['project'] = 'test'
1050 task.add_annotation('I should really do this task')
1051 self.assertEqual(task['project'], 'test')
1053 self.assertEqual(task['project'], 'test')
1055 def test_serialize_annotations(self):
1056 # Test that serializing annotations is possible
1057 t = Task(self.tw, description="test")
1060 t.add_annotation("annotation1")
1061 t.add_annotation("annotation2")
1063 data = t._serialize('annotations', t._data['annotations'])
1065 self.assertEqual(len(data), 2)
1066 self.assertEqual(type(data[0]), dict)
1067 self.assertEqual(type(data[1]), dict)
1069 self.assertEqual(data[0]['description'], "annotation1")
1070 self.assertEqual(data[1]['description'], "annotation2")
1073 class UnicodeTest(TasklibTest):
1075 def test_unicode_task(self):
1076 Task(self.tw, description=six.u("†åßk")).save()
1079 def test_filter_by_unicode_task(self):
1080 Task(self.tw, description=six.u("†åßk")).save()
1081 tasks = self.tw.tasks.filter(description=six.u("†åßk"))
1082 self.assertEqual(len(tasks), 1)
1084 def test_non_unicode_task(self):
1085 Task(self.tw, description="test task").save()
1088 class ReadOnlyDictViewTest(unittest.TestCase):
1091 self.sample = dict(l=[1,2,3], d={'k':'v'})
1092 self.original_sample = copy.deepcopy(self.sample)
1093 self.view = ReadOnlyDictView(self.sample)
1095 def test_readonlydictview_getitem(self):
1097 self.assertEqual(l, self.sample['l'])
1099 # Assert that modification changed only copied value
1101 self.assertNotEqual(l, self.sample['l'])
1103 # Assert that viewed dict is not changed
1104 self.assertEqual(self.sample, self.original_sample)
1106 def test_readonlydictview_contains(self):
1107 self.assertEqual('l' in self.view, 'l' in self.sample)
1108 self.assertEqual('d' in self.view, 'd' in self.sample)
1109 self.assertEqual('k' in self.view, 'k' in self.sample)
1111 # Assert that viewed dict is not changed
1112 self.assertEqual(self.sample, self.original_sample)
1114 def test_readonlydictview_iter(self):
1115 self.assertEqual(list(k for k in self.view),
1116 list(k for k in self.sample))
1118 # Assert the view is correct after modification
1119 self.sample['new'] = 'value'
1120 self.assertEqual(list(k for k in self.view),
1121 list(k for k in self.sample))
1123 def test_readonlydictview_len(self):
1124 self.assertEqual(len(self.view), len(self.sample))
1126 # Assert the view is correct after modification
1127 self.sample['new'] = 'value'
1128 self.assertEqual(len(self.view), len(self.sample))
1130 def test_readonlydictview_get(self):
1131 l = self.view.get('l')
1132 self.assertEqual(l, self.sample.get('l'))
1134 # Assert that modification changed only copied value
1136 self.assertNotEqual(l, self.sample.get('l'))
1138 # Assert that viewed dict is not changed
1139 self.assertEqual(self.sample, self.original_sample)
1141 def test_readonlydict_items(self):
1142 view_items = self.view.items()
1143 sample_items = list(self.sample.items())
1144 self.assertEqual(view_items, sample_items)
1146 view_items.append('newkey')
1147 self.assertNotEqual(view_items, sample_items)
1148 self.assertEqual(self.sample, self.original_sample)
1150 def test_readonlydict_values(self):
1151 view_values = self.view.values()
1152 sample_values = list(self.sample.values())
1153 self.assertEqual(view_values, sample_values)
1155 view_list_item = list(filter(lambda x: type(x) is list,
1157 view_list_item.append(4)
1158 self.assertNotEqual(view_values, sample_values)
1159 self.assertEqual(self.sample, self.original_sample)
1162 class LazyUUIDTaskTest(TasklibTest):
1165 super(LazyUUIDTaskTest, self).setUp()
1167 self.stored = Task(self.tw, description="this is test task")
1170 self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
1172 def test_uuid_non_conversion(self):
1173 assert self.stored['uuid'] == self.lazy['uuid']
1174 assert type(self.lazy) is LazyUUIDTask
1176 def test_lazy_explicit_conversion(self):
1177 assert type(self.lazy) is LazyUUIDTask
1179 assert type(self.lazy) is Task
1181 def test_conversion_key(self):
1182 assert self.stored['description'] == self.lazy['description']
1183 assert type(self.lazy) is Task
1185 def test_conversion_attribute(self):
1186 assert type(self.lazy) is LazyUUIDTask
1187 assert self.lazy.completed is False
1188 assert type(self.lazy) is Task
1190 def test_normal_to_lazy_equality(self):
1191 assert self.stored == self.lazy
1192 assert not self.stored != self.lazy
1193 assert type(self.lazy) is LazyUUIDTask
1195 def test_lazy_to_lazy_equality(self):
1196 lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1197 lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
1199 assert lazy1 == lazy2
1200 assert not lazy1 != lazy2
1201 assert type(lazy1) is LazyUUIDTask
1202 assert type(lazy2) is LazyUUIDTask
1204 def test_normal_to_lazy_inequality(self):
1205 # Create a different UUID by changing the last letter
1206 wrong_uuid = self.stored['uuid']
1207 wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1209 wrong_lazy = LazyUUIDTask(self.tw, wrong_uuid)
1211 assert not self.stored == wrong_lazy
1212 assert self.stored != wrong_lazy
1213 assert type(wrong_lazy) is LazyUUIDTask
1215 def test_lazy_to_lazy_inequality(self):
1216 # Create a different UUID by changing the last letter
1217 wrong_uuid = self.stored['uuid']
1218 wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1220 lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1221 lazy2 = LazyUUIDTask(self.tw, wrong_uuid)
1223 assert not lazy1 == lazy2
1224 assert lazy1 != lazy2
1225 assert type(lazy1) is LazyUUIDTask
1226 assert type(lazy2) is LazyUUIDTask
1228 def test_lazy_in_queryset(self):
1229 tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
1231 assert self.lazy in tasks
1232 assert type(self.lazy) is LazyUUIDTask
1234 def test_lazy_saved(self):
1235 assert self.lazy.saved is True
1237 def test_lazy_modified(self):
1238 assert self.lazy.modified is False
1240 def test_lazy_modified_fields(self):
1241 assert self.lazy._modified_fields == set()
1244 class LazyUUIDTaskSetTest(TasklibTest):
1247 super(LazyUUIDTaskSetTest, self).setUp()
1249 self.task1 = Task(self.tw, description="task 1")
1250 self.task2 = Task(self.tw, description="task 2")
1251 self.task3 = Task(self.tw, description="task 3")
1263 self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1265 def test_length(self):
1266 assert len(self.lazy) == 3
1267 assert type(self.lazy) is LazyUUIDTaskSet
1269 def test_contains(self):
1270 assert self.task1 in self.lazy
1271 assert self.task2 in self.lazy
1272 assert self.task3 in self.lazy
1273 assert type(self.lazy) is LazyUUIDTaskSet
1275 def test_eq_lazy(self):
1276 new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1277 assert self.lazy == new_lazy
1278 assert not self.lazy != new_lazy
1279 assert type(self.lazy) is LazyUUIDTaskSet
1281 def test_eq_real(self):
1282 assert self.lazy == self.tw.tasks.all()
1283 assert self.tw.tasks.all() == self.lazy
1284 assert not self.lazy != self.tw.tasks.all()
1286 assert type(self.lazy) is LazyUUIDTaskSet
1288 def test_union(self):
1289 taskset = set([self.task1])
1290 lazyset = LazyUUIDTaskSet(
1292 (self.task2['uuid'], self.task3['uuid'])
1295 assert taskset | lazyset == self.lazy
1296 assert lazyset | taskset == self.lazy
1297 assert taskset.union(lazyset) == self.lazy
1298 assert lazyset.union(taskset) == self.lazy
1301 assert lazyset == self.lazy
1303 def test_difference(self):
1304 taskset = set([self.task1, self.task2])
1305 lazyset = LazyUUIDTaskSet(
1307 (self.task2['uuid'], self.task3['uuid'])
1310 assert taskset - lazyset == set([self.task1])
1311 assert lazyset - taskset == set([self.task3])
1312 assert taskset.difference(lazyset) == set([self.task1])
1313 assert lazyset.difference(taskset) == set([self.task3])
1316 assert lazyset == set([self.task3])
1318 def test_symmetric_difference(self):
1319 taskset = set([self.task1, self.task2])
1320 lazyset = LazyUUIDTaskSet(
1322 (self.task2['uuid'], self.task3['uuid'])
1325 assert taskset ^ lazyset == set([self.task1, self.task3])
1326 assert lazyset ^ taskset == set([self.task1, self.task3])
1327 assert taskset.symmetric_difference(lazyset) == set([self.task1, self.task3])
1328 assert lazyset.symmetric_difference(taskset) == set([self.task1, self.task3])
1331 assert lazyset == set([self.task1, self.task3])
1333 def test_intersection(self):
1334 taskset = set([self.task1, self.task2])
1335 lazyset = LazyUUIDTaskSet(
1337 (self.task2['uuid'], self.task3['uuid'])
1340 assert taskset & lazyset == set([self.task2])
1341 assert lazyset & taskset == set([self.task2])
1342 assert taskset.intersection(lazyset) == set([self.task2])
1343 assert lazyset.intersection(taskset) == set([self.task2])
1346 assert lazyset == set([self.task2])
1349 class TaskWarriorBackendTest(TasklibTest):
1351 def test_config(self):
1352 assert self.tw.config['nag'] == "You have more urgent tasks."
1353 assert self.tw.config['debug'] == "no"