# General information about the project.
project = u'tasklib'
-copyright = u'2013, Rob Golding'
+copyright = u'2014, Rob Golding'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = '0.1'
+version = '0.7'
# The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '0.7'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
>>> tw = TaskWarrior(data_location='~/.task', create=True)
+Creating Tasks
+--------------
+
+To create a task, simply create a new ``Task`` object::
+
+ >>> new_task = Task(tw, description="throw out the trash")
+
+This task is not yet saved to TaskWarrior (same as in Django), not until
+you call ``.save()`` method::
+
+ >>> new_task.save()
+
+You can set any attribute as a keyword argument to the Task object::
+
+ >>> complex_task = Task(tw, description="finally fix the shower", due=datetime(2015,2,14,8,0,0), priority='H')
+
+or by setting the attributes one by one::
+
+ >>> complex_task = Task(tw)
+ >>> complex_task['description'] = "finally fix the shower"
+ >>> complex_task['due'] = datetime(2015,2,14,8,0,0)
+ >>> complex_task['priority'] = 'H'
+
+Modifying Task
+--------------
+
+To modify a created or retrieved ``Task`` object, use dictionary-like access::
+
+ >>> homework = tw.tasks.get(tags=['chores'])
+ >>> homework['project'] = 'Home'
+
+The change is not propagated to the TaskWarrior until you run the ``save()`` method::
+
+ >>> homework.save()
+
+Attributes, which map to native Python objects are converted. See Task Attributes section.
+
+Task Attributes
+---------------
+
+Attributes of task objects are accessible through indices, like so::
+
+ >>> task = tw.tasks.pending().get(tags__contain='work') # There is only one pending task with 'work' tag
+ >>> task['description']
+ 'Upgrade Ubuntu Server'
+ >>> task['id']
+ 15
+ >>> task['due']
+ datetime.datetime(2013, 12, 5, 0, 0)
+ >>> task['tags']
+ ['work', 'servers']
+
+The following fields are deserialized into Python objects:
+
+* ``due``, ``wait``, ``scheduled``, ``until``, ``entry``: deserialized to a ``datetime`` object
+* ``annotations``: deserialized to a list of ``TaskAnnotation`` objects
+* ``tags``: deserialized to a list of strings
+* ``depends``: deserialized to a set of ``Task`` objects
+
+Attributes should be set using the correct Python representation, which will be
+serialized into the correct format when the task is saved.
+
+Operations on Tasks
+-------------------
+
+After modifying one or more attributes, simple call ``save()`` to write those
+changes to the database::
+
+ >>> task = tw.tasks.pending().get(tags__contain='work')
+ >>> task['due'] = datetime(year=2014, month=1, day=5)
+ >>> task.save()
+
+To mark a task as complete, use ``done()``::
+
+ >>> task = tw.tasks.pending().get(tags__contain='work')
+ >>> task.done()
+ >>> len(tw.tasks.pending().filter(tags__contain='work'))
+ 0
+
+To delete a task, use ``delete()``::
+
+ >>> task = tw.tasks.get(description="task added by mistake")
+ >>> task.delete()
+
+To update a task object with values from TaskWarrior database, use ``refresh()``. Example::
+
+ >>> task = Task(tw, description="learn to cook")
+ >>> task.save()
+ >>> task['id']
+ 5
+ >>> task['tags']
+ []
+
+Now, suppose the we modify the task using the TaskWarrior interface in another terminal::
+
+ $ task 5 modify +someday
+ Task 5 modified.
+
+Switching back to the open python process::
+
+ >>> task['tags']
+ []
+ >>> task.refresh()
+ >>> task['tags']
+ ['someday']
+
+
Retrieving Tasks
----------------
API. To get all tasks (including completed ones)::
>>> tw.tasks.all()
+ ['First task', 'Completed task', 'Deleted task', ...]
Filtering
---------
Filter tasks using the same familiar syntax::
- >>> tw.tasks.filter(status='pending', tags__contain='work')
+ >>> tw.tasks.filter(status='pending', tags__contains=['work'])
['Upgrade Ubuntu Server']
Filter arguments are passed to the ``task`` command (``__`` is replaced by
>>> tw.tasks.filter('status:pending +work')
['Upgrade Ubuntu Server']
+Although this practice is discouraged, as by using raw commands you may lose
+some of the portablility of your commands over different TaskWarrior versions.
+
+However, you can mix raw commands with keyword filters, as in the given example::
+
+ >>> tw.tasks.filter('+BLOCKING', project='Home') # Gets all blocking tasks in project Home
+ ['Fix the toilette']
+
+This can be a neat way how to use syntax not yet supported by tasklib. The above
+is excellent example, since virtual tags do not work the same way as the ordinary ones, that is::
+
+ >>> tw.tasks.filter(tags=['BLOCKING'])
+ >>> []
+
+will not work.
+
There are built-in functions for retrieving pending & completed tasks::
>>> tw.tasks.pending().filter(tags__contain='work')
Use ``get()`` to return the only task in a ``TaskQuerySet``, or raise an
exception::
- >>> tw.tasks.filter(status='pending', tags__contain='work').get()
- 'Upgrade Ubuntu Server'
- >>> tw.tasks.filter(status='pending', tags__contain='work').get(status='completed')
+ >>> tw.tasks.get(tags__contain='work')['status']
+ 'pending'
+ >>> tw.tasks.get(status='completed', tags__contains='work') # Status of only task with the work tag is pending, so this should fail
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "tasklib/task.py", line 224, in get
'Lookup parameters were {0}'.format(kwargs))
- tasklib.task.DoesNotExist: Task matching query does not exist. Lookup parameters were {'status': 'completed'}
- >>> tw.tasks.filter(status='pending', tags__contain='home').get()
+ tasklib.task.DoesNotExist: Task matching query does not exist. Lookup parameters were {'status': 'completed', 'tags__contains': ['work']}
+ >>> tw.tasks.get(status='pending')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "tasklib/task.py", line 227, in get
'Lookup parameters were {1}'.format(num, kwargs))
- ValueError: get() returned more than one Task -- it returned 2! Lookup parameters were {}
+ ValueError: get() returned more than one Task -- it returned 23! Lookup parameters were {'status': 'pending'}
-Task Attributes
----------------
+Additionally, since filters return ``TaskQuerySets`` you can stack filters on top of each other::
-Attributes of task objects are accessible through indices, like so::
+ >>> home_tasks = tw.tasks.filter(project='Wife')
+ >>> home_tasks.filter(due__before=datetime(2015,2,14,14,14,14)) # What I have to do until Valentine's day
+ ['Prepare surprise birthday party']
- >>> task = tw.tasks.pending().filter(tags__contain='work').get()
- >>> task['description']
- 'Upgrade Ubuntu Server'
- >>> task['id']
- 15
- >>> task['due']
- datetime.datetime(2013, 12, 5, 0, 0)
- >>> task['tags']
- ['work', 'servers']
+Equality of Task objects
+------------------------
-The following fields are deserialized into Python objects:
+Two Tasks are considered equal if they have the same UUIDs::
-* ``due``: deserialized to a ``datetime`` object
-* ``annotations``: deserialized to a list of dictionaries, where the ``entry``
- field is a ``datetime`` object
-* ``tags``: deserialized to a list
+ >>> task1 = Task(tw, description="Pet the dog")
+ >>> task1.save()
+ >>> task2 = tw.tasks.get(description="Pet the dog")
+ >>> task1 == task2
+ True
-Attributes should be set using the correct Python representation, which will be
-serialized into the correct format when the task is saved.
+If you compare the two unsaved tasks, they are considered equal only if it's the
+same Python object::
-Saving Tasks
-------------
+ >>> task1 = Task(tw, description="Pet the cat")
+ >>> task2 = Task(tw, description="Pet the cat")
+ >>> task1 == task2
+ False
+ >>> task3 = task1
+ >>> task3 == task1
+ True
-After modifying one or more attributes, simple call ``save()`` to write those
-changes to the database::
+Working with annotations
+------------------------
+
+Annotations of the tasks are represented in tasklib by ``TaskAnnotation`` objects. These
+are much like ``Task`` objects, albeit very simplified.
+
+ >>> annotated_task = tw.tasks.get(description='Annotated task')
+ >>> annotated_task['annotations']
+ [Yeah, I am annotated!]
+
+Annotations have only defined ``entry`` and ``description`` values::
+
+ >>> annotation = annotated_task['annotations'][0]
+ >>> annotation['entry']
+ datetime.datetime(2015, 1, 3, 21, 13, 55)
+ >>> annotation['description']
+ u'Yeah, I am annotated!'
+
+To add a annotation to a Task, use ``add_annotation()``::
+
+ >>> task = Task(tw, description="new task")
+ >>> task.add_annotation("we can annotate any task")
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "build/bdist.linux-x86_64/egg/tasklib/task.py", line 355, in add_annotation
+ tasklib.task.NotSaved: Task needs to be saved to add annotation
+
+However, Task needs to be saved before you can add a annotation to it::
- >>> task = tw.tasks.pending().filter(tags__contain='work').get()
- >>> task['due'] = datetime(year=2014, month=1, day=5)
>>> task.save()
+ >>> task.add_annotation("we can annotate saved tasks")
+ >>> task['annotations']
+ [we can annotate saved tasks]
-To mark a task as complete, use ``done()``::
+To remove the annotation, pass its description to ``remove_annotation()`` method::
+
+ >>> task.remove_annotation("we can annotate saved tasks")
+
+Alternatively, you can pass the ``TaskAnnotation`` object itself::
+
+ >>> task.remove_annotation(task['annotations'][0])
+
+
+Running custom commands
+-----------------------
+
+To run a custom commands, use ``execute_command()`` method of ``TaskWarrior`` object::
+
+ >>> tw = TaskWarrior()
+ >>> tw.execute_command(['log', 'Finish high school.'])
+ [u'Logged task.']
+
+You can use ``config_override`` keyword argument to specify a dictionary of configuration overrides::
+
+ >>> tw.execute_command(['3', 'done'], config_override={'gc': 'off'}) # Will mark 3 as completed and it will retain its ID
+
+Setting custom configuration values
+-----------------------------------
+
+By default, TaskWarrior does not use any of configuration values stored in
+your .taskrc. To see what configuration values are passed to each executed
+task command, have a peek into ``config`` attribute of ``TaskWarrior`` object::
+
+ >>> tw.config
+ {'confirmation': 'no', 'data.location': '/home/tbabej/.task'}
+
+To pass your own configuration, you just need to update this dictionary::
+
+ >>> tw.config.update({'hooks': 'off'}) # tasklib will not trigger hooks
+
+Working with UDAs
+-----------------
+
+Since TaskWarrior does not read your .taskrc, you need to define any UDAs
+in the TaskWarrior's config dictionary, as described above.
+
+Let us demonstrate this on the same example as in the TaskWarrior's docs::
+
+ >>> tw = TaskWarrior()
+ >>> tw.config.update({'uda.estimate.type': 'numeric'})
+
+Now we can filter and create tasks using the estimate UDA::
+
+ >>> task = Task(tw, description="Long task", estimate=1000)
+ >>> task.save()
+ >>> task['id']
+ 1
+
+This is saved as UDA in the TaskWarrior::
+
+ $ task 1 export
+ {"id":1,"description":"Long task","estimate":1000, ...}
+
+As long as ``TaskWarrior``'s config is updated, we can approach UDAs as built in attributes::
+
+ >>> tw.tasks.filter(estimate=1000)
+ Long task
+
+Syncing
+-------
+
+Syncing is not directly supported by tasklib, but it can be made to work in a similiar way
+as the UDAs. First we need to update the ``config`` dictionary by the values required for
+sync to work, and then we can run the sync command using the ``execute_command()`` method::
+
+ >>> tw = TaskWarrior()
+ >>> sync_config = {
+ ... 'taskd.certificate': '/home/tbabej/.task/tbabej.cert.pem',
+ ... 'taskd.credentials': 'Public/tbabej/34af54de-3cb2-4d3d-82be-33ddb8fd3e66',
+ ... 'taskd.server': 'task.server.com:53589',
+ ... 'taskd.ca': '/home/tbabej/.task/ca.cert.pem',
+ ... 'taskd.trust': 'ignore hostname'}
+ >>> tw.config.update(sync_config)
+ >>> tw.execute_command(['sync'])
- >>> task = tw.tasks.pending().filter(tags__contain='work').get()
- >>> task.done()
- >>> len(tw.tasks.pending().filter(tags__contain='work'))
- 0
.. _taskwarrior: http://taskwarrior.org
pass
-class TaskResource(object):
+class SerializingObject(object):
+ """
+ Common ancestor for TaskResource & TaskFilter, since they both
+ need to serialize arguments.
+ """
+
+ def _deserialize(self, key, value):
+ hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
+ lambda x: x if x != '' else None)
+ return hydrate_func(value)
+
+ def _serialize(self, key, value):
+ dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
+ lambda x: x if x is not None else '')
+ return dehydrate_func(value)
+
+ def timestamp_serializer(self, date):
+ if not date:
+ return None
+ return date.strftime(DATE_FORMAT)
+
+ def timestamp_deserializer(self, date_str):
+ if not date_str:
+ return None
+ return datetime.datetime.strptime(date_str, DATE_FORMAT)
+
+ def serialize_entry(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_entry(self, value):
+ return self.timestamp_deserializer(value)
+
+ def serialize_modified(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_modified(self, value):
+ return self.timestamp_deserializer(value)
+
+ def serialize_due(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_due(self, value):
+ return self.timestamp_deserializer(value)
+
+ def serialize_scheduled(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_scheduled(self, value):
+ return self.timestamp_deserializer(value)
+
+ def serialize_until(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_until(self, value):
+ return self.timestamp_deserializer(value)
+
+ def serialize_wait(self, value):
+ return self.timestamp_serializer(value)
+
+ def deserialize_wait(self, value):
+ return self.timestamp_deserializer(value)
+
+ def deserialize_annotations(self, data):
+ return [TaskAnnotation(self, d) for d in data] if data else []
+
+ def serialize_tags(self, tags):
+ return ','.join(tags) if tags else ''
+
+ def deserialize_tags(self, tags):
+ if isinstance(tags, basestring):
+ return tags.split(',') if tags else []
+ return tags
+
+ def serialize_depends(self, cur_dependencies):
+ # Return the list of uuids
+ return ','.join(task['uuid'] for task in cur_dependencies)
+
+ def deserialize_depends(self, raw_uuids):
+ raw_uuids = raw_uuids or '' # Convert None to empty string
+ uuids = raw_uuids.split(',')
+ return set(self.warrior.tasks.get(uuid=uuid) for uuid in uuids if uuid)
+
+
+class TaskResource(SerializingObject):
read_only_fields = []
def _load_data(self, data):
self._data = data
+ # We need to use a copy for original data, so that changes
+ # 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):
- hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
- lambda x: x)
- return hydrate_func(self._data.get(key))
+ # This is a workaround to make TaskResource non-iterable
+ # over simple index-based iteration
+ try:
+ int(key)
+ raise StopIteration
+ except ValueError:
+ pass
+
+ 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)
- dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
- lambda x: x)
- self._data[key] = dehydrate_func(value)
- self._modified_fields.add(key)
+ self._data[key] = self._serialize(key, value)
def __str__(self):
s = six.text_type(self.__unicode__())
self.task = task
self._load_data(data)
- def deserialize_entry(self, data):
- return datetime.datetime.strptime(data, DATE_FORMAT) if data else None
-
- def serialize_entry(self, date):
- return date.strftime(DATE_FORMAT) if date else ''
-
def remove(self):
self.task.remove_annotation(self)
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)
+ # Check that user is not able to set read-only value in __init__
+ for key in kwargs.keys():
+ if key in self.read_only_fields:
+ raise RuntimeError('Field \'%s\' is read-only' % key)
- self._load_data(kwargs)
- self._modified_fields = set()
+ # 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
+
+ # 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):
+ 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):
+ 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):
+ 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 completed(self):
return self['status'] == six.text_type('completed')
def saved(self):
return self['uuid'] is not None or self['id'] is not None
- def serialize_due(self, date):
- return date.strftime(DATE_FORMAT)
+ def serialize_depends(self, cur_dependencies):
+ # Check that all the tasks are saved
+ for task in cur_dependencies:
+ if not task.saved:
+ raise Task.NotSaved('Task \'%s\' needs to be saved before '
+ 'it can be set as dependency.' % task)
- def deserialize_due(self, date_str):
- if not date_str:
- return None
- return datetime.datetime.strptime(date_str, DATE_FORMAT)
+ return super(Task, self).serialize_depends(cur_dependencies)
- def deserialize_annotations(self, data):
- return [TaskAnnotation(self, d) for d in data] if data else []
+ def format_depends(self):
+ # We need to generate added and removed dependencies list,
+ # since Taskwarrior does not accept redefining dependencies.
- def deserialize_tags(self, tags):
- if isinstance(tags, basestring):
- return tags.split(',') if tags else []
- return tags
+ # This cannot be part of serialize_depends, since we need
+ # to keep a list of all depedencies in the _data dictionary,
+ # not just currently added/removed ones
- def serialize_tags(self, tags):
- return ','.join(tags) if tags else ''
+ old_dependencies_raw = self._original_data.get('depends','')
+ old_dependencies = self.deserialize_depends(old_dependencies_raw)
+
+ added = self['depends'] - old_dependencies
+ removed = old_dependencies - self['depends']
+
+ # Removed dependencies need to be prefixed with '-'
+ return 'depends:' + ','.join(
+ [t['uuid'] for t in added] +
+ ['-' + t['uuid'] for t in removed]
+ )
+
+ 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:
if self.deleted:
raise Task.DeletedTask("Task was already deleted")
- self.warrior.execute_command([self['uuid'], 'delete'], config_override={
- 'confirmation': 'no',
- })
+ self.warrior.execute_command([self['uuid'], 'delete'])
# Refresh the status again, so that we have updated info stored
self.refresh(only_fields=['status'])
# Circumvent the ID storage, since ID is considered read-only
self._data['id'] = int(id_lines[0].split(' ')[2].rstrip('.'))
- self._modified_fields.clear()
self.refresh()
def add_annotation(self, annotation):
def remove_annotation(self, annotation):
if not self.saved:
- raise Task.NotSaved("Task needs to be saved to add annotation")
+ raise Task.NotSaved("Task needs to be saved to remove annotation")
if isinstance(annotation, TaskAnnotation):
annotation = annotation['description']
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])
- 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._update_data(to_update, update_original=True)
else:
- self._data = new_data
+ self._load_data(new_data)
-class TaskFilter(object):
+class TaskFilter(SerializingObject):
"""
A set of parameters to filter the task list with.
"""
# Replace the value with empty string, since that is the
# convention in TW for empty values
- value = value if value is not None else ''
+ attribute_key = key.split('.')[0]
+ value = self._serialize(attribute_key, value)
# If we are filtering by uuid:, do not use uuid keyword
# due to TW-1452 bug
if key == 'uuid':
self.filter_params.insert(0, value)
else:
- self.filter_params.append('{0}:{1}'.format(key, value))
+ # Surround value with aphostrophes unless it's a empty string
+ value = "'%s'" % value if value else ''
+
+ # We enforce equality match by using 'is' (or 'none') modifier
+ # Without using this syntax, filter fails due to TW-1479
+ modifier = '.is' if value else '.none'
+ key = key + modifier if '.' not in key else key
+
+ self.filter_params.append("{0}:{1}".format(key, value))
def get_filter_params(self):
return [f for f in self.filter_params if f]
os.makedirs(data_location)
self.config = {
'data.location': os.path.expanduser(data_location),
+ 'confirmation': 'no',
+ 'dependency.confirmation': 'no', # See TW-1483 or taskrc man page
}
self.tasks = TaskQuerySet(self)
self.version = self._get_version()
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
})
def undo(self):
- self.execute_command(['undo'], config_override={
- 'confirmation': 'no',
- })
+ self.execute_command(['undo'])
# coding=utf-8
+import datetime
import shutil
import tempfile
import unittest
no_priority_task = self.tw.tasks.get(priority=None)
self.assertEqual(no_priority_task['description'], "no priority task")
+ def test_filter_for_task_with_space_in_descripition(self):
+ task = Task(self.tw, description="test task")
+ task.save()
+
+ filtered_task = self.tw.tasks.get(description="test task")
+ self.assertEqual(filtered_task['description'], "test task")
+
+ def test_filter_for_task_without_space_in_descripition(self):
+ task = Task(self.tw, description="test")
+ task.save()
+
+ filtered_task = self.tw.tasks.get(description="test")
+ self.assertEqual(filtered_task['description'], "test")
+
+ def test_filter_for_task_with_space_in_project(self):
+ task = Task(self.tw, description="test", project="random project")
+ task.save()
+
+ filtered_task = self.tw.tasks.get(project="random project")
+ self.assertEqual(filtered_task['project'], "random project")
+
+ def test_filter_for_task_without_space_in_project(self):
+ task = Task(self.tw, description="test", project="random")
+ task.save()
+
+ filtered_task = self.tw.tasks.get(project="random")
+ self.assertEqual(filtered_task['project'], "random")
+
class TaskTest(TasklibTest):
self.assertRaises(Task.DeletedTask, t.done)
+ def test_modify_simple_attribute_without_space(self):
+ t = Task(self.tw, description="test")
+ t.save()
+
+ self.assertEquals(t['description'], "test")
+
+ t['description'] = "test-modified"
+ t.save()
+
+ self.assertEquals(t['description'], "test-modified")
+
+ def test_modify_simple_attribute_with_space(self):
+ # Space can pose problems with parsing
+ t = Task(self.tw, description="test task")
+ t.save()
+
+ self.assertEquals(t['description'], "test task")
+
+ t['description'] = "test task modified"
+ t.save()
+
+ self.assertEquals(t['description'], "test task modified")
+
+ def test_empty_dependency_set_of_unsaved_task(self):
+ t = Task(self.tw, description="test task")
+ self.assertEqual(t['depends'], set())
+
+ def test_empty_dependency_set_of_saved_task(self):
+ t = Task(self.tw, description="test task")
+ t.save()
+ self.assertEqual(t['depends'], set())
+
+ def test_set_unsaved_task_as_dependency(self):
+ # Adds only one dependency to task with no dependencies
+ t = Task(self.tw, description="test task")
+ dependency = Task(self.tw, description="needs to be done first")
+
+ # We only save the parent task, dependency task is unsaved
+ t.save()
+
+ self.assertRaises(Task.NotSaved,
+ t.__setitem__, 'depends', set([dependency]))
+
+ def test_set_simple_dependency_set(self):
+ # Adds only one dependency to task with no dependencies
+ t = Task(self.tw, description="test task")
+ dependency = Task(self.tw, description="needs to be done first")
+
+ t.save()
+ dependency.save()
+
+ t['depends'] = set([dependency])
+
+ self.assertEqual(t['depends'], set([dependency]))
+
+ def test_set_complex_dependency_set(self):
+ # Adds two dependencies to task with no dependencies
+ t = Task(self.tw, description="test task")
+ dependency1 = Task(self.tw, description="needs to be done first")
+ dependency2 = Task(self.tw, description="needs to be done second")
+
+ t.save()
+ dependency1.save()
+ dependency2.save()
+
+ t['depends'] = set([dependency1, dependency2])
+
+ self.assertEqual(t['depends'], set([dependency1, dependency2]))
+
+ def test_remove_from_dependency_set(self):
+ # Removes dependency from task with two dependencies
+ t = Task(self.tw, description="test task")
+ dependency1 = Task(self.tw, description="needs to be done first")
+ dependency2 = Task(self.tw, description="needs to be done second")
+
+ dependency1.save()
+ dependency2.save()
+
+ t['depends'] = set([dependency1, dependency2])
+ t.save()
+
+ t['depends'] = t['depends'] - set([dependency2])
+ t.save()
+
+ self.assertEqual(t['depends'], set([dependency1]))
+
+ def test_add_to_dependency_set(self):
+ # Adds dependency to task with one dependencies
+ t = Task(self.tw, description="test task")
+ dependency1 = Task(self.tw, description="needs to be done first")
+ dependency2 = Task(self.tw, description="needs to be done second")
+
+ dependency1.save()
+ dependency2.save()
+
+ t['depends'] = set([dependency1])
+ t.save()
+
+ t['depends'] = t['depends'] | set([dependency2])
+ t.save()
+
+ self.assertEqual(t['depends'], set([dependency1, dependency2]))
+
+ def test_simple_dependency_set_save_repeatedly(self):
+ # Adds only one dependency to task with no dependencies
+ t = Task(self.tw, description="test task")
+ dependency = Task(self.tw, description="needs to be done first")
+ dependency.save()
+
+ t['depends'] = set([dependency])
+ t.save()
+
+ # We taint the task, but keep depends intact
+ t['description'] = "test task modified"
+ t.save()
+
+ self.assertEqual(t['depends'], set([dependency]))
+
+ # We taint the task, but assign the same set to the depends
+ t['depends'] = set([dependency])
+ t['description'] = "test task modified again"
+ t.save()
+
+ self.assertEqual(t['depends'], set([dependency]))
+
+ def test_compare_different_tasks(self):
+ # Negative: compare two different tasks
+ t1 = Task(self.tw, description="test task")
+ t2 = Task(self.tw, description="test task")
+
+ t1.save()
+ t2.save()
+
+ self.assertEqual(t1 == t2, False)
+
+ def test_compare_same_task_object(self):
+ # Compare Task object wit itself
+ t = Task(self.tw, description="test task")
+ t.save()
+
+ self.assertEqual(t == t, True)
+
+ def test_compare_same_task(self):
+ # Compare the same task using two different objects
+ t1 = Task(self.tw, description="test task")
+ t1.save()
+
+ t2 = self.tw.tasks.get(uuid=t1['uuid'])
+ self.assertEqual(t1 == t2, True)
+
+ def test_compare_unsaved_tasks(self):
+ # t1 and t2 are unsaved tasks, considered to be unequal
+ # despite the content of data
+ t1 = Task(self.tw, description="test task")
+ t2 = Task(self.tw, description="test task")
+
+ self.assertEqual(t1 == t2, False)
+
+ def test_hash_unsaved_tasks(self):
+ # Considered equal, it's the same object
+ t1 = Task(self.tw, description="test task")
+ t2 = t1
+ self.assertEqual(hash(t1) == hash(t2), True)
+
+ def test_hash_same_task(self):
+ # Compare the hash of the task using two different objects
+ t1 = Task(self.tw, description="test task")
+ t1.save()
+
+ t2 = self.tw.tasks.get(uuid=t1['uuid'])
+ self.assertEqual(t1.__hash__(), t2.__hash__())
+
+ def test_adding_task_with_priority(self):
+ t = Task(self.tw, description="test task", priority="M")
+ t.save()
+
+ def test_removing_priority_with_none(self):
+ t = Task(self.tw, description="test task", priority="L")
+ t.save()
+
+ # Remove the priority mark
+ t['priority'] = None
+ t.save()
+
+ # Assert that priority is not there after saving
+ self.assertEqual(t['priority'], None)
+
+ def test_adding_task_with_due_time(self):
+ t = Task(self.tw, description="test task", due=datetime.datetime.now())
+ t.save()
+
+ def test_removing_due_time_with_none(self):
+ t = Task(self.tw, description="test task", due=datetime.datetime.now())
+ t.save()
+
+ # Remove the due timestamp
+ t['due'] = None
+ t.save()
+
+ # Assert that due timestamp is no longer there
+ self.assertEqual(t['due'], None)
+
+ def test_modified_fields_new_task(self):
+ t = Task(self.tw)
+
+ # This should be empty with new task
+ self.assertEqual(set(t._modified_fields), set())
+
+ # Modify the task
+ t['description'] = "test task"
+ self.assertEqual(set(t._modified_fields), set(['description']))
+
+ t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
+ self.assertEqual(set(t._modified_fields), set(['description', 'due']))
+
+ t['project'] = "test project"
+ self.assertEqual(set(t._modified_fields),
+ set(['description', 'due', 'project']))
+
+ # List of modified fields should clear out when saved
+ t.save()
+ self.assertEqual(set(t._modified_fields), set())
+
+ # Reassigning the fields with the same values now should not produce
+ # modified fields
+ t['description'] = "test task"
+ t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
+ t['project'] = "test project"
+ self.assertEqual(set(t._modified_fields), set())
+
+ def test_modified_fields_loaded_task(self):
+ t = Task(self.tw)
+
+ # Modify the task
+ t['description'] = "test task"
+ t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
+ t['project'] = "test project"
+
+ dependency = Task(self.tw, description="dependency")
+ dependency.save()
+ t['depends'] = set([dependency])
+
+ # List of modified fields should clear out when saved
+ t.save()
+ self.assertEqual(set(t._modified_fields), set())
+
+ # Get the task by using a filter by UUID
+ t2 = self.tw.tasks.get(uuid=t['uuid'])
+
+ # Reassigning the fields with the same values now should not produce
+ # modified fields
+ t['description'] = "test task"
+ t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
+ t['project'] = "test project"
+ t['depends'] = set([dependency])
+ 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:
+ kwargs = {'description': 'test task', readonly_key: 'value'}
+ self.assertRaises(RuntimeError,
+ lambda: Task(self.tw, **kwargs))
+
+ def test_setting_read_only_attrs_through_setitem(self):
+ # Test that we are unable to set readonly attrs through __init__
+ for readonly_key in Task.read_only_fields:
+ t = Task(self.tw, description='test task')
+ self.assertRaises(RuntimeError,
+ lambda: t.__setitem__(readonly_key, 'value'))
+
class AnnotationTest(TasklibTest):