]> git.madduck.net Git - etc/taskwarrior.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Merge branch 'master' into develop
authorRob Golding <rob@robgolding.com>
Sun, 31 Jan 2016 18:28:10 +0000 (18:28 +0000)
committerRob Golding <rob@robgolding.com>
Sun, 31 Jan 2016 18:28:10 +0000 (18:28 +0000)
.travis.yml
tasklib/backends.py
tasklib/lazy.py [new file with mode: 0644]
tasklib/serializing.py
tasklib/tests.py

index 7d2ce8fa3818e25ec2bd5d86d18063bd40d2115a..f57978793963f4e0730ead4aa5fb747a678a529f 100644 (file)
@@ -9,13 +9,13 @@ env:
   - TASK_VERSION=v2.4.2
   - TASK_VERSION=v2.4.3
   - TASK_VERSION=v2.4.4
-  - TASK_VERSION=2.5.0
+  - TASK_VERSION=v2.5.0
+  - TASK_VERSION=v2.5.1
 python:
-  - "2.6"
   - "2.7"
-  - "3.2"
   - "3.3"
   - "3.4"
+  - "3.5"
 install:
   - pip install -e .
   - pip install coveralls
index b15e8bd97afac69339d6cdeb64bce43d953616bc..76fce88054d53e2a9b5b89e83ee4be9e8125d7ae 100644 (file)
@@ -75,7 +75,7 @@ class Backend(object):
         Converts TW syntax datetime string to a localized datetime
         object. This method is not mandatory.
         """
-        raise NotImplemented
+        raise NotImplementedError
 
 
 class TaskWarriorException(Exception):
diff --git a/tasklib/lazy.py b/tasklib/lazy.py
new file mode 100644 (file)
index 0000000..aa2c065
--- /dev/null
@@ -0,0 +1,216 @@
+"""
+Provides lazy implementations for Task and TaskQuerySet.
+"""
+
+
+class LazyUUIDTask(object):
+    """
+    A lazy wrapper around Task object, referenced by UUID.
+
+    - Supports comparison with LazyUUIDTask or Task objects (equality by UUIDs)
+    - If any attribute other than 'uuid' requested, a lookup in the
+      backend will be performed and this object will be replaced by a proper
+      Task object.
+    """
+
+    def __init__(self, tw, uuid):
+        self._tw = tw
+        self._uuid = uuid
+
+    def __getitem__(self, key):
+        # LazyUUIDTask does not provide anything else other than 'uuid'
+        if key is 'uuid':
+            return self._uuid
+        else:
+            self.replace()
+            return self[key]
+
+    def __getattr__(self, name):
+        # Getattr is called only if the attribute could not be found using
+        # normal means
+        self.replace()
+        return getattr(self, name)
+
+    def __eq__(self, other):
+        if other and other['uuid']:
+            # For saved Tasks, just define equality by equality of uuids
+            return self['uuid'] == other['uuid']
+
+    def __hash__(self):
+        return self['uuid'].__hash__()
+
+    def __repr__(self):
+        return "LazyUUIDTask: {0}".format(self._uuid)
+
+    @property
+    def saved(self):
+        """
+        Implementation of the 'saved' property. Always returns True.
+        """
+        return True
+
+    @property
+    def _modified_fields(self):
+        return set()
+
+    @property
+    def modified(self):
+        return False
+
+    def replace(self):
+        """
+        Performs conversion to the regular Task object, referenced by the
+        stored UUID.
+        """
+
+        replacement = self._tw.tasks.get(uuid=self._uuid)
+        self.__class__ = replacement.__class__
+        self.__dict__ = replacement.__dict__
+
+
+class LazyUUIDTaskSet(object):
+    """
+    A lazy wrapper around TaskQuerySet object, for tasks referenced by UUID.
+
+    - Supports 'in' operator with LazyUUIDTask or Task objects
+    - If iteration over the objects in the LazyUUIDTaskSet is requested, the
+      LazyUUIDTaskSet will be converted to QuerySet and evaluated
+    """
+
+    def __init__(self, tw, uuids):
+        self._tw = tw
+        self._uuids = set(uuids)
+
+    def __getattr__(self, name):
+        # Getattr is called only if the attribute could not be found using
+        # normal means
+
+        if name.startswith('__'):
+            # If some internal method was being search, do not convert
+            # to TaskQuerySet just because of that
+            raise AttributeError
+        else:
+            self.replace()
+            return getattr(self, name)
+
+    def __repr__(self):
+        return "LazyUUIDTaskSet([{0}])".format(', '.join(self._uuids))
+
+    def __eq__(self, other):
+        return set(t['uuid'] for t in other) == self._uuids
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __contains__(self, task):
+        return task['uuid'] in self._uuids
+
+    def __len__(self):
+        return len(self._uuids)
+
+    def __iter__(self):
+        for uuid in self._uuids:
+            yield LazyUUIDTask(self._tw, uuid)
+
+    def __sub__(self, other):
+        return self.difference(other)
+
+    def __isub__(self, other):
+        return self.difference_update(other)
+
+    def __rsub__(self, other):
+        return LazyUUIDTaskSet(self._tw,
+            set(t['uuid'] for t in other) - self._uuids)
+
+    def __or__(self, other):
+        return self.union(other)
+
+    def __ior__(self, other):
+        return self.update(other)
+
+    def __ror__(self, other):
+        return self.union(other)
+
+    def __xor__(self, other):
+        return self.symmetric_difference(other)
+
+    def __ixor__(self, other):
+        return self.symmetric_difference_update(other)
+
+    def __rxor__(self, other):
+        return self.symmetric_difference(other)
+
+    def __and__(self, other):
+        return self.intersection(other)
+
+    def __iand__(self, other):
+        return self.intersection_update(other)
+
+    def __rand__(self, other):
+        return self.intersection(other)
+
+    def __le__(self, other):
+        return self.issubset(other)
+
+    def __ge__(self, other):
+        return self.issuperset(other)
+
+    def issubset(self, other):
+        return all([task in other for task in self])
+
+    def issuperset(self, other):
+        return all([task in self for task in other])
+
+    def union(self, other):
+        return LazyUUIDTaskSet(self._tw,
+            self._uuids | set(t['uuid'] for t in other))
+
+    def intersection(self, other):
+        return LazyUUIDTaskSet(self._tw,
+            self._uuids & set(t['uuid'] for t in other))
+
+    def difference(self, other):
+        return LazyUUIDTaskSet(self._tw,
+            self._uuids - set(t['uuid'] for t in other))
+
+    def symmetric_difference(self, other):
+        return LazyUUIDTaskSet(self._tw,
+            self._uuids ^ set(t['uuid'] for t in other))
+
+    def update(self, other):
+        self._uuids |= set(t['uuid'] for t in other)
+        return self
+
+    def intersection_update(self, other):
+        self._uuids &= set(t['uuid'] for t in other)
+        return self
+
+    def difference_update(self, other):
+        self._uuids -= set(t['uuid'] for t in other)
+        return self
+
+    def symmetric_difference_update(self, other):
+        self._uuids ^= set(t['uuid'] for t in other)
+        return self
+
+    def add(self, task):
+        self._uuids.add(task['uuid'])
+
+    def remove(self, task):
+        self._uuids.remove(task['uuid'])
+
+    def pop(self):
+        return self._uuids.pop()
+
+    def clear(self):
+        self._uuids.clear()
+
+    def replace(self):
+        """
+        Performs conversion to the regular TaskQuerySet object, referenced by
+        the stored UUIDs.
+        """
+
+        replacement = self._tw.tasks.filter(' '.join(self._uuids))
+        self.__class__ = replacement.__class__
+        self.__dict__ = replacement.__dict__
index 767f7df7cc47f3cb52dfd0f85a084b1af4e0bea5..8cdeaf286bc8a44cf9c4fafdb7cd689162273ac2 100644 (file)
@@ -5,6 +5,9 @@ import pytz
 import six
 import tzlocal
 
+
+from .lazy import LazyUUIDTaskSet, LazyUUIDTask
+
 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
 local_zone = tzlocal.get_localzone()
 
@@ -175,17 +178,30 @@ class SerializingObject(object):
 
     def deserialize_tags(self, tags):
         if isinstance(tags, six.string_types):
-            return tags.split(',') if tags else []
-        return tags or []
+            return set(tags.split(',')) if tags else set()
+        return set(tags or [])
+
+    def serialize_parent(self, parent):
+        return parent['uuid'] if parent else ''
+
+    def deserialize_parent(self, uuid):
+        return LazyUUIDTask(self.backend, uuid) if uuid else None
 
     def serialize_depends(self, value):
         # Return the list of uuids
         value = value if value is not None else set()
-        return ','.join(task['uuid'] for task in value)
+
+        if isinstance(value, LazyUUIDTaskSet):
+            return ','.join(value._uuids)
+        else:
+            return ','.join(task['uuid'] for task in value)
 
     def deserialize_depends(self, raw_uuids):
         raw_uuids = raw_uuids or []  # Convert None to empty list
 
+        if not raw_uuids:
+            return set()
+
         # TW 2.4.4 encodes list of dependencies as a single string
         if type(raw_uuids) is not list:
             uuids = raw_uuids.split(',')
@@ -193,7 +209,7 @@ class SerializingObject(object):
         else:
             uuids = raw_uuids
 
-        return set(self.backend.tasks.get(uuid=uuid) for uuid in uuids if uuid)
+        return LazyUUIDTaskSet(self.backend, uuids)
 
     def datetime_normalizer(self, value):
         """
index bf4fb0c07c8ee2acc1c1c9ae98c5964ac3b333ac..b0e342d8348ddd9e451d78d9ecf7f16887e9b5af 100644 (file)
@@ -12,7 +12,8 @@ import tempfile
 import unittest
 
 from .backends import TaskWarrior
-from .task import Task, ReadOnlyDictView
+from .task import Task, ReadOnlyDictView, TaskQuerySet
+from .lazy import LazyUUIDTask, LazyUUIDTaskSet
 from .serializing import DATE_FORMAT, local_zone
 
 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
@@ -695,16 +696,24 @@ class TaskTest(TasklibTest):
     def test_adding_tag_by_appending(self):
         t = Task(self.tw, description="test task", tags=['test1'])
         t.save()
-        t['tags'].append('test2')
+        t['tags'].add('test2')
         t.save()
-        self.assertEqual(t['tags'], ['test1', 'test2'])
+        self.assertEqual(t['tags'], set(['test1', 'test2']))
+
+    def test_adding_tag_twice(self):
+        t = Task(self.tw, description="test task", tags=['test1'])
+        t.save()
+        t['tags'].add('test2')
+        t['tags'].add('test2')
+        t.save()
+        self.assertEqual(t['tags'], set(['test1', 'test2']))
 
     def test_adding_tag_by_appending_empty(self):
         t = Task(self.tw, description="test task")
         t.save()
-        t['tags'].append('test')
+        t['tags'].add('test')
         t.save()
-        self.assertEqual(t['tags'], ['test'])
+        self.assertEqual(t['tags'], set(['test']))
 
     def test_serializers_returning_empty_string_for_none(self):
         # Test that any serializer returns '' when passed None
@@ -736,6 +745,15 @@ class TaskTest(TasklibTest):
         t.save()
         self.assertEqual(len(self.tw.tasks.pending()), 2)
 
+    def test_spawned_task_parent(self):
+        today = datetime.date.today()
+        t = Task(self.tw, description="brush teeth",
+                 due=today, recur="daily")
+        t.save()
+
+        spawned = self.tw.tasks.pending().get(due=today)
+        assert spawned['parent'] == t
+
     def test_modify_number_of_tasks_at_once(self):
         for i in range(1, 100):
             Task(self.tw, description="test task %d" % i, tags=['test']).save()
@@ -1101,3 +1119,171 @@ class ReadOnlyDictViewTest(unittest.TestCase):
         view_list_item.append(4)
         self.assertNotEqual(view_values, sample_values)
         self.assertEqual(self.sample, self.original_sample)
+
+
+class LazyUUIDTaskTest(TasklibTest):
+
+    def setUp(self):
+        super(LazyUUIDTaskTest, self).setUp()
+
+        self.stored = Task(self.tw, description="this is test task")
+        self.stored.save()
+
+        self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
+
+    def test_uuid_non_conversion(self):
+        assert self.stored['uuid'] == self.lazy['uuid']
+        assert type(self.lazy) is LazyUUIDTask
+
+    def test_lazy_explicit_conversion(self):
+        assert type(self.lazy) is LazyUUIDTask
+        self.lazy.replace()
+        assert type(self.lazy) is Task
+
+    def test_conversion_key(self):
+        assert self.stored['description'] == self.lazy['description']
+        assert type(self.lazy) is Task
+
+    def test_conversion_attribute(self):
+        assert type(self.lazy) is LazyUUIDTask
+        assert self.lazy.completed is False
+        assert type(self.lazy) is Task
+
+    def test_normal_to_lazy_equality(self):
+        assert self.stored == self.lazy
+        assert type(self.lazy) is LazyUUIDTask
+
+    def test_lazy_to_lazy_equality(self):
+        lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
+        lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
+
+        assert lazy1 == lazy2
+        assert type(lazy1) is LazyUUIDTask
+        assert type(lazy2) is LazyUUIDTask
+
+    def test_lazy_in_queryset(self):
+        tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
+
+        assert self.lazy in tasks
+        assert type(self.lazy) is LazyUUIDTask
+
+    def test_lazy_saved(self):
+        assert self.lazy.saved is True
+
+    def test_lazy_modified(self):
+        assert self.lazy.modified is False
+
+    def test_lazy_modified_fields(self):
+        assert self.lazy._modified_fields == set()
+
+
+class LazyUUIDTaskSetTest(TasklibTest):
+
+    def setUp(self):
+        super(LazyUUIDTaskSetTest, self).setUp()
+
+        self.task1 = Task(self.tw, description="task 1")
+        self.task2 = Task(self.tw, description="task 2")
+        self.task3 = Task(self.tw, description="task 3")
+
+        self.task1.save()
+        self.task2.save()
+        self.task3.save()
+
+        self.uuids = (
+            self.task1['uuid'],
+            self.task2['uuid'],
+            self.task3['uuid']
+        )
+
+        self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
+
+    def test_length(self):
+        assert len(self.lazy) == 3
+        assert type(self.lazy) is LazyUUIDTaskSet
+
+    def test_contains(self):
+        assert self.task1 in self.lazy
+        assert self.task2 in self.lazy
+        assert self.task3 in self.lazy
+        assert type(self.lazy) is LazyUUIDTaskSet
+
+    def test_eq_lazy(self):
+        new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
+        assert self.lazy == new_lazy
+        assert not self.lazy != new_lazy
+        assert type(self.lazy) is LazyUUIDTaskSet
+
+    def test_eq_real(self):
+        assert self.lazy == self.tw.tasks.all()
+        assert self.tw.tasks.all() == self.lazy
+        assert not self.lazy != self.tw.tasks.all()
+
+        assert type(self.lazy) is LazyUUIDTaskSet
+
+    def test_union(self):
+        taskset = set([self.task1])
+        lazyset = LazyUUIDTaskSet(
+            self.tw,
+            (self.task2['uuid'], self.task3['uuid'])
+        )
+
+        assert taskset | lazyset == self.lazy
+        assert lazyset | taskset == self.lazy
+        assert taskset.union(lazyset) == self.lazy
+        assert lazyset.union(taskset) == self.lazy
+
+        lazyset |= taskset
+        assert lazyset == self.lazy
+
+    def test_difference(self):
+        taskset = set([self.task1, self.task2])
+        lazyset = LazyUUIDTaskSet(
+            self.tw,
+            (self.task2['uuid'], self.task3['uuid'])
+        )
+
+        assert taskset - lazyset == set([self.task1])
+        assert lazyset - taskset == set([self.task3])
+        assert taskset.difference(lazyset) == set([self.task1])
+        assert lazyset.difference(taskset) == set([self.task3])
+
+        lazyset -= taskset
+        assert lazyset == set([self.task3])
+
+    def test_symmetric_difference(self):
+        taskset = set([self.task1, self.task2])
+        lazyset = LazyUUIDTaskSet(
+            self.tw,
+            (self.task2['uuid'], self.task3['uuid'])
+        )
+
+        assert taskset ^ lazyset == set([self.task1, self.task3])
+        assert lazyset ^ taskset == set([self.task1, self.task3])
+        assert taskset.symmetric_difference(lazyset) == set([self.task1, self.task3])
+        assert lazyset.symmetric_difference(taskset) == set([self.task1, self.task3])
+
+        lazyset ^= taskset
+        assert lazyset == set([self.task1, self.task3])
+
+    def test_intersection(self):
+        taskset = set([self.task1, self.task2])
+        lazyset = LazyUUIDTaskSet(
+            self.tw,
+            (self.task2['uuid'], self.task3['uuid'])
+        )
+
+        assert taskset & lazyset == set([self.task2])
+        assert lazyset & taskset == set([self.task2])
+        assert taskset.intersection(lazyset) == set([self.task2])
+        assert lazyset.intersection(taskset) == set([self.task2])
+
+        lazyset &= taskset
+        assert lazyset == set([self.task2])
+
+
+class TaskWarriorBackendTest(TasklibTest):
+
+    def test_config(self):
+        assert self.tw.config['nag'] == "You have more urgent tasks."
+        assert self.tw.config['debug'] == "no"