X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/58f3ecf4ed9351d3226b3ac0152ec56313f79431..a39f27ce1b92af11794fde4f0b7badc8a39d0d6f:/tasklib/task.py?ds=sidebyside diff --git a/tasklib/task.py b/tasklib/task.py index f61fb05..9b3626d 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -5,6 +5,7 @@ import json import logging import os import six +import sys import subprocess DATE_FORMAT = '%Y%m%dT%H%M%SZ' @@ -28,6 +29,18 @@ class SerializingObject(object): """ Common ancestor for TaskResource & TaskFilter, since they both need to serialize arguments. + + Serializing method should hold the following contract: + - any empty value (meaning removal of the attribute) + is deserialized into a empty string + - None denotes a empty value for any attribute + + Deserializing method should hold the following contract: + - None denotes a empty value for any attribute (however, + this is here as a safeguard, TaskWarrior currently does + 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. """ def _deserialize(self, key, value): @@ -42,7 +55,7 @@ class SerializingObject(object): def timestamp_serializer(self, date): if not date: - return None + return '' return date.strftime(DATE_FORMAT) def timestamp_deserializer(self, date_str): @@ -97,9 +110,10 @@ class SerializingObject(object): return tags.split(',') if tags else [] return tags or [] - def serialize_depends(self, cur_dependencies): + def serialize_depends(self, value): # Return the list of uuids - return ','.join(task['uuid'] for task in cur_dependencies) + value = value if value is not None else set() + return ','.join(task['uuid'] for task in value) def deserialize_depends(self, raw_uuids): raw_uuids = raw_uuids or '' # Convert None to empty string @@ -205,6 +219,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 @@ -257,7 +305,7 @@ class Task(TaskResource): yield key @property - def _is_modified(self): + def modified(self): return bool(list(self._modified_fields)) @property @@ -282,7 +330,7 @@ class Task(TaskResource): def serialize_depends(self, cur_dependencies): # Check that all the tasks are saved - for task in cur_dependencies: + for task in (cur_dependencies or set()): if not task.saved: raise Task.NotSaved('Task \'%s\' needs to be saved before ' 'it can be set as dependency.' % task) @@ -350,7 +398,7 @@ class Task(TaskResource): 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'] @@ -370,6 +418,9 @@ class Task(TaskResource): # 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): @@ -396,13 +447,21 @@ class Task(TaskResource): def add_field(field): # Add the output of format_field method to args list (defaults to # field:value) - serialized_value = self._serialize(field, self._data[field]) or '' - format_default = lambda: "{0}:{1}".format( - field, - "'{0}'".format(serialized_value) if serialized_value else '' - ) + serialized_value = self._serialize(field, self._data[field]) + + # Empty values should not be enclosed in quotation marks, see + # TW-1510 + if serialized_value is '': + escaped_serialized_value = '' + else: + escaped_serialized_value = "'{0}'".format(serialized_value) + + format_default = lambda: "{0}:{1}".format(field, + escaped_serialized_value) + format_func = getattr(self, 'format_{0}'.format(field), format_default) + args.append(format_func()) # If we're modifying saved task, simply pass on all modified fields @@ -435,6 +494,20 @@ class Task(TaskResource): 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): """