]> 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 pull request #24 from tbabej/hooks2
authorTomas Babej <tomasbabej@gmail.com>
Sat, 17 Jan 2015 10:39:24 +0000 (11:39 +0100)
committerTomas Babej <tomasbabej@gmail.com>
Sat, 17 Jan 2015 10:39:24 +0000 (11:39 +0100)
Better hook support

1  2 
tasklib/task.py
tasklib/tests.py

diff --combined tasklib/task.py
index f61fb056591c3afb22486ad62a30f78c586c8918,0b18a14a5d1bcdbe1f77cc58344a1695e1fa79a8..6de828f92cd458702690516de6548c3a1635c8ff
@@@ -5,6 -5,7 +5,7 @@@ import jso
  import logging
  import os
  import six
+ import sys
  import subprocess
  
  DATE_FORMAT = '%Y%m%dT%H%M%SZ'
@@@ -205,6 -206,40 +206,40 @@@ class Task(TaskResource)
          """
          pass
  
+     @classmethod
+     def from_input(cls, input_file=sys.stdin, modify=None):
+         """
+         Creates a Task object, directly from the stdin, by reading one line.
+         If modify=True, two lines are used, first line interpreted as the
+         original state of the Task object, and second line as its new,
+         modified value. This is consistent with the TaskWarrior's hook
+         system.
+         Object created by this method should not be saved, deleted
+         or refreshed, as t could create a infinite loop. For this
+         reason, TaskWarrior instance is set to None.
+         Input_file argument can be used to specify the input file,
+         but defaults to sys.stdin.
+         """
+         # TaskWarrior instance is set to None
+         task = cls(None)
+         # Detect the hook type if not given directly
+         name = os.path.basename(sys.argv[0])
+         modify = name.startswith('on-modify') if modify is None else modify
+         # Load the data from the input
+         task._load_data(json.loads(input_file.readline().strip()))
+         # If this is a on-modify event, we are provided with additional
+         # line of input, which provides updated data
+         if modify:
+             task._update_data(json.loads(input_file.readline().strip()))
+         return task
      def __init__(self, warrior, **kwargs):
          self.warrior = warrior
  
      def _modified_fields(self):
          writable_fields = set(self._data.keys()) - set(self.read_only_fields)
          for key in writable_fields:
 -            if self._data.get(key) != self._original_data.get(key):
 +            new_value = self._data.get(key)
 +            old_value = self._original_data.get(key)
 +
 +            # Make sure not to mark data removal as modified field if the
 +            # field originally had some empty value
 +            if key in self._data and not new_value and not old_value:
 +                continue
 +
 +            if new_value != old_value:
                  yield key
  
      @property
-     def _is_modified(self):
+     def modified(self):
          return bool(list(self._modified_fields))
  
      @property
          self.refresh(only_fields=['status'])
  
      def save(self):
-         if self.saved and not self._is_modified:
+         if self.saved and not self.modified:
              return
  
          args = [self['uuid'], 'modify'] if self.saved else ['add']
              # Circumvent the ID storage, since ID is considered read-only
              self._data['id'] = int(id_lines[0].split(' ')[2].rstrip('.'))
  
+         # Refreshing is very important here, as not only modification time
+         # is updated, but arbitrary attribute may have changed due hooks
+         # altering the data before saving
          self.refresh()
  
      def add_annotation(self, annotation):
          else:
              self._load_data(new_data)
  
+     def export_data(self):
+         """
+         Exports current data contained in the Task as JSON
+         """
+         # We need to remove spaces for TW-1504, use custom separators
+         data_tuples = ((key, self._serialize(key, value))
+                        for key, value in six.iteritems(self._data))
+         # Empty string denotes empty serialized value, we do not want
+         # to pass that to TaskWarrior.
+         data_tuples = filter(lambda t: t[1] is not '', data_tuples)
+         data = dict(data_tuples)
+         return json.dumps(data, separators=(',',':'))
  
  class TaskFilter(SerializingObject):
      """
diff --combined tasklib/tests.py
index 35979a8c889b7743cc533db79ea6f02432e101f8,cba3271cf7c2090dfd6a9fd7cba5d0892828d857..d498f099c28a3678fec396bb4cdce16cdba93124
@@@ -1,35 -1,14 +1,37 @@@
  # coding=utf-8
  
  import datetime
+ import itertools
+ import six
  import shutil
  import tempfile
  import unittest
  
  from .task import TaskWarrior, Task
  
 +# http://taskwarrior.org/docs/design/task.html , Section: The Attributes
 +TASK_STANDARD_ATTRS = (
 +    'status',
 +    'uuid',
 +    'entry',
 +    'description',
 +    'start',
 +    'end',
 +    'due',
 +    'until',
 +    'wait',
 +    'modified',
 +    'scheduled',
 +    'recur',
 +    'mask',
 +    'imask',
 +    'parent',
 +    'project',
 +    'priority',
 +    'depends',
 +    'tags',
 +    'annotation',
 +)
  
  class TasklibTest(unittest.TestCase):
  
@@@ -434,14 -413,6 +436,14 @@@ class TaskTest(TasklibTest)
          t['depends'] = set([dependency])
          self.assertEqual(set(t._modified_fields), set())
  
 +    def test_modified_fields_not_affected_by_reading(self):
 +        t = Task(self.tw)
 +
 +        for field in TASK_STANDARD_ATTRS:
 +            value = t[field]
 +
 +        self.assertEqual(set(t._modified_fields), set())
 +
      def test_setting_read_only_attrs_through_init(self):
          # Test that we are unable to set readonly attrs through __init__
          for readonly_key in Task.read_only_fields:
          self.assertEqual(t['tags'], ['test'])
  
  
+ class TaskFromHookTest(TasklibTest):
+     input_add_data = six.StringIO(
+         '{"description":"Buy some milk",'
+         '"entry":"20141118T050231Z",'
+         '"status":"pending",'
+         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
+     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
+         '{"description":"Buy some milk finally",'
+         '"entry":"20141118T050231Z",'
+         '"status":"completed",'
+         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
+     exported_raw_data = (
+         '{"project":"Home",'
+          '"due":"20150101T232323Z",'
+          '"description":"test task"}')
+     def test_setting_up_from_add_hook_input(self):
+         t = Task.from_input(input_file=self.input_add_data)
+         self.assertEqual(t['description'], "Buy some milk")
+         self.assertEqual(t.pending, True)
+     def test_setting_up_from_modified_hook_input(self):
+         t = Task.from_input(input_file=self.input_modify_data, modify=True)
+         self.assertEqual(t['description'], "Buy some milk finally")
+         self.assertEqual(t.pending, False)
+         self.assertEqual(t.completed, True)
+         self.assertEqual(t._original_data['status'], "pending")
+         self.assertEqual(t._original_data['description'], "Buy some milk")
+         self.assertEqual(set(t._modified_fields),
+                          set(['status', 'description']))
+     def test_export_data(self):
+         t = Task(self.tw, description="test task",
+             project="Home", due=datetime.datetime(2015,1,1,23,23,23))
+         # Check that the output is a permutation of:
+         # {"project":"Home","description":"test task","due":"20150101232323Z"}
+         allowed_segments = self.exported_raw_data[1:-1].split(',')
+         allowed_output = [
+             '{' + ','.join(segments) + '}'
+             for segments in itertools.permutations(allowed_segments)
+         ]
+         self.assertTrue(any(t.export_data() == expected
+                             for expected in allowed_output))
  class AnnotationTest(TasklibTest):
  
      def setUp(self):