def _load_data(self, data):
self._data = data
# We need to use a copy for original data, so that changes
- # are not propagated
+ # are not propagated. Shallow copy is alright, since data dict uses only
+ # primitive data types
self._original_data = data.copy()
+ def _update_data(self, data, update_original=False):
+ """
+ Low level update of the internal _data dict. Data which are coming as
+ updates should already be serialized. If update_original is True, the
+ original_data dict is updated as well.
+ """
+
+ self._data.update(data)
+
+ if update_original:
+ self._original_data.update(data)
+
def __getitem__(self, key):
# This is a workaround to make TaskResource non-iterable
# over simple index-based iteration
except ValueError:
pass
- hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
- lambda x: x)
- return hydrate_func(self._data.get(key))
+ return self._deserialize(key, self._data.get(key))
def __setitem__(self, key, value):
if key in self.read_only_fields:
raise RuntimeError('Field \'%s\' is read-only' % key)
+ self._data[key] = self._serialize(key, value)
+
+ def _deserialize(self, key, value):
+ hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
+ lambda x: x)
+ return hydrate_func(value)
+
+ def _serialize(self, key, value):
dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
lambda x: x)
- self._data[key] = dehydrate_func(value)
+ return dehydrate_func(value)
def __str__(self):
s = six.text_type(self.__unicode__())
class Task(TaskResource):
- read_only_fields = ['id', 'entry', 'urgency', 'uuid']
+ read_only_fields = ['id', 'entry', 'urgency', 'uuid', 'modified']
class DoesNotExist(Exception):
pass
"""
pass
- def __init__(self, warrior, data={}, **kwargs):
+ def __init__(self, warrior, **kwargs):
self.warrior = warrior
- # We keep data for backwards compatibility
- kwargs.update(data)
+ # We serialize the data in kwargs so that users of the library
+ # do not have to pass different data formats via __setitem__ and
+ # __init__ methods, that would be confusing
- self._load_data(kwargs)
+ # Rather unfortunate syntax due to python2.6 comaptiblity
+ self._load_data(dict((key, self._serialize(key, value))
+ for (key, value) in six.iteritems(kwargs)))
def __unicode__(self):
return self['description']
def __eq__(self, other):
- return self['uuid'] == other['uuid']
+ if self['uuid'] and other['uuid']:
+ # For saved Tasks, just define equality by equality of uuids
+ return self['uuid'] == other['uuid']
+ else:
+ # If the tasks are not saved, compare the actual instances
+ return id(self) == id(other)
+
def __hash__(self):
- return self['uuid'].__hash__()
+ if self['uuid']:
+ # For saved Tasks, just define equality by equality of uuids
+ return self['uuid'].__hash__()
+ else:
+ # If the tasks are not saved, return hash of instance id
+ return id(self).__hash__()
@property
def _modified_fields(self):
- for key in self._data.keys():
+ 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
return self['uuid'] is not None or self['id'] is not None
def serialize_due(self, date):
+ if not date:
+ return None
return date.strftime(DATE_FORMAT)
def deserialize_due(self, date_str):
removed = old_dependencies - self['depends']
# Removed dependencies need to be prefixed with '-'
- return ','.join(
+ return 'depends:' + ','.join(
[t['uuid'] for t in added] +
['-' + t['uuid'] for t in removed]
)
def serialize_tags(self, tags):
return ','.join(tags) if tags else ''
+ def format_description(self):
+ # Task version older than 2.4.0 ignores first word of the
+ # task description if description: prefix is used
+ if self.warrior.version < VERSION_2_4_0:
+ return self._data['description']
+ else:
+ return "description:'{0}'".format(self._data['description'] or '')
+
def delete(self):
if not self.saved:
raise Task.NotSaved("Task needs to be saved before it can be deleted")
args = []
def add_field(field):
- # Task version older than 2.4.0 ignores first word of the
- # task description if description: prefix is used
- if self.warrior.version < VERSION_2_4_0 and field == 'description':
- args.append(self._data[field])
- elif field == 'depends':
- args.append('{0}:{1}'.format(field, self.format_depends()))
- else:
- args.append('{0}:{1}'.format(field, self._data[field]))
+ # Add the output of format_field method to args list (defaults to
+ # field:value)
+ format_default = lambda k: "{0}:'{1}'".format(k, self._data[k] or '')
+ format_func = getattr(self, 'format_{0}'.format(field),
+ lambda: format_default(field))
+ args.append(format_func())
# If we're modifying saved task, simply pass on all modified fields
if self.saved:
if only_fields:
to_update = dict(
[(k, new_data.get(k)) for k in only_fields])
- self._data.update(to_update)
- self._original_data.update(to_update)
+ self._update_data(to_update, update_original=True)
else:
- self._data = new_data
- # We need to create a clone for original_data though
- # Shallow copy is alright, since data dict uses only
- # primitive data types
- self._original_data = new_data.copy()
+ self._load_data(new_data)
class TaskFilter(object):
if line:
data = line.strip(',')
try:
- tasks.append(Task(self, json.loads(data)))
+ filtered_task = Task(self)
+ filtered_task._load_data(json.loads(data))
+ tasks.append(filtered_task)
except ValueError:
raise TaskWarriorException('Invalid JSON: %s' % data)
return tasks