X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/a0df8b516ffc77612d4ef8193f2bd5dd696916fa..a40915e62131c8828305ec59ed630c63e6abecd7:/tasklib/task.py diff --git a/tasklib/task.py b/tasklib/task.py index 0b18a14..f482f53 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -29,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): @@ -43,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): @@ -98,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 @@ -159,6 +172,40 @@ class TaskResource(SerializingObject): def __repr__(self): return str(self) + 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=(',',':')) + + @property + def _modified_fields(self): + writable_fields = set(self._data.keys()) - set(self.read_only_fields) + for key in writable_fields: + 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 modified(self): + return bool(list(self._modified_fields)) + class TaskAnnotation(TaskResource): read_only_fields = ['entry', 'description'] @@ -276,17 +323,6 @@ class Task(TaskResource): # If the tasks are not saved, return hash of instance id return id(self).__hash__() - @property - 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): - yield key - - @property - def modified(self): - return bool(list(self._modified_fields)) - @property def completed(self): return self['status'] == six.text_type('completed') @@ -309,7 +345,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) @@ -426,13 +462,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 @@ -465,21 +509,6 @@ 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): """ A set of parameters to filter the task list with. @@ -641,7 +670,8 @@ class TaskWarrior(object): self.config = { 'data.location': os.path.expanduser(data_location), 'confirmation': 'no', - 'dependency.confirmation': 'no', # See TW-1483 or taskrc man page + 'dependency.confirmation': 'no', # See TW-1483 or taskrc man page + 'recurrence.confirmation': 'no', # Necessary for modifying R tasks } self.tasks = TaskQuerySet(self) self.version = self._get_version()