]> git.madduck.net Git - etc/taskwarrior.git/blobdiff - tasklib/task.py

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:

SerializingObject: Document purpose of normalizing methods
[etc/taskwarrior.git] / tasklib / task.py
index 73280b0cf447bb6e1f1f246914ac0916ae2130ec..537c79291363edee5e2dec8679c2873af936fe17 100644 (file)
@@ -28,6 +28,40 @@ class TaskWarriorException(Exception):
     pass
 
 
+class ReadOnlyDictView(object):
+    """
+    Provides simplified read-only view upon dict object.
+    """
+
+    def __init__(self, viewed_dict):
+        self.viewed_dict = viewed_dict
+
+    def __getitem__(self, key):
+        return copy.deepcopy(self.viewed_dict.__getitem__(key))
+
+    def __contains__(self, k):
+        return self.viewed_dict.__contains__(k)
+
+    def __iter__(self):
+        for value in self.viewed_dict:
+            yield copy.deepcopy(value)
+
+    def __len__(self):
+        return len(self.viewed_dict)
+
+    def get(self, key, default=None):
+        return copy.deepcopy(self.viewed_dict.get(key, default))
+
+    def has_key(self, key):
+        return self.viewed_dict.has_key(key)
+
+    def items(self):
+        return [copy.deepcopy(v) for v in self.viewed_dict.items()]
+
+    def values(self):
+        return [copy.deepcopy(v) for v in self.viewed_dict.values()]
+
+
 class SerializingObject(object):
     """
     Common ancestor for TaskResource & TaskFilter, since they both
@@ -44,6 +78,15 @@ class SerializingObject(object):
         not export empty-valued attributes) if the attribute
         is not iterable (e.g. list or set), in which case
         a empty iterable should be used.
+
+    Normalizing methods should hold the following contract:
+      - They are used to validate and normalize the user input.
+        Any attribute value that comes from the user (during Task
+        initialization, assignign values to Task attributes, or
+        filtering by user-provided values of attributes) is first
+        validated and normalized using the normalize_{key} method.
+      - If validation or normalization fails, normalizer is expected
+        to raise ValueError.
     """
 
     def _deserialize(self, key, value):
@@ -195,7 +238,13 @@ class SerializingObject(object):
             localized = value
         
         return localized
-            
+
+    def normalize_uuid(self, value):
+        # Enforce sane UUID
+        if not isinstance(value, six.text_type) or value == '':
+            raise ValueError("UUID must be a valid non-empty string.")
+
+        return value
 
 
 class TaskResource(SerializingObject):
@@ -384,6 +433,9 @@ class Task(TaskResource):
                           for (key, value) in six.iteritems(kwargs))
         self._original_data = copy.deepcopy(self._data)
 
+        # Provide read only access to the original data
+        self.original = ReadOnlyDictView(self._original_data)
+
     def __unicode__(self):
         return self['description']
 
@@ -777,14 +829,14 @@ class TaskWarrior(object):
         stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
         return stdout.strip('\n')
 
-    def execute_command(self, args, config_override={}):
+    def execute_command(self, args, config_override={}, allow_failure=True):
         command_args = self._get_command_args(
             args, config_override=config_override)
         logger.debug(' '.join(command_args))
         p = subprocess.Popen(command_args, stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE)
         stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
-        if p.returncode:
+        if p.returncode and allow_failure:
             if stderr.strip():
                 error_msg = stderr.strip().splitlines()[-1]
             else:
@@ -792,7 +844,15 @@ class TaskWarrior(object):
             raise TaskWarriorException(error_msg)
         return stdout.strip().split('\n')
 
+    def enforce_recurrence(self):
+        # Run arbitrary report command which will trigger generation
+        # of recurrent tasks.
+        # TODO: Make a version dependant enforcement once
+        #       TW-1531 is handled
+        self.execute_command(['next'], allow_failure=False)
+
     def filter_tasks(self, filter_obj):
+        self.enforce_recurrence()
         args = ['export', '--'] + filter_obj.get_filter_params()
         tasks = []
         for line in self.execute_command(args):