--- /dev/null
+ [report]
+ omit = */tests.py
+ exclude_lines =
+     raise NotImplementedError
+     @abc.abstractmethod
 
--- /dev/null
+ .cache
+ *.pyc
+ *~
+ tasklib.egg-info
+ /dist
+ /build
+ docs/_build
+ .project
+ .coverage
 
--- /dev/null
+ language: python
+ env:
+   - TASK_VERSION=v2.1.1
+   - TASK_VERSION=v2.1.2
+   - TASK_VERSION=v2.2.0
+   - TASK_VERSION=v2.3.0
+   - TASK_VERSION=v2.4.0
+   - TASK_VERSION=v2.4.1
+   - TASK_VERSION=v2.4.2
+   - TASK_VERSION=v2.4.3
+   - TASK_VERSION=v2.4.4
+   - TASK_VERSION=v2.5.0
+   - TASK_VERSION=v2.5.1
+ python:
+   - "2.7"
+   - "3.3"
+   - "3.4"
+   - "3.5"
+   - "3.6"
+ install:
+   - pip install -e .
+   - pip install coveralls
+   - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
+   - sudo apt-get update -qq
+   - sudo apt-get install -qq build-essential cmake uuid-dev g++-4.8
+   - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 50
+   - git clone --recursive https://github.com/GothenburgBitFactory/taskwarrior
+   - cd taskwarrior
+   - git checkout $TASK_VERSION
+   - git clean -dfx
+   - git submodule init
+   - git submodule update
+   - cmake -DCMAKE_BUILD_TYPE=release .
+   - make -j2
+   - sudo make install
+   - task --version
+ before_script:
+   - cd $TRAVIS_BUILD_DIR
+ script:
+   - coverage run --source=tasklib setup.py test
+ after_success:
+   - coveralls
 
--- /dev/null
+ Rob Golding
+ Tomas Babej
 
--- /dev/null
+ Copyright (c) 2013-2017, Rob Golding. All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ 
+     * Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+     * Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+     * Neither the name of Rob Golding, nor the names of its contributors may
+       be used to endorse or promote products derived from this software
+       without specific prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
--- /dev/null
+ include LICENSE
+ include README.rst
 
--- /dev/null
+ tasklib
+ =======
+ 
+ .. image:: https://travis-ci.org/robgolding63/tasklib.png?branch=develop
+     :target: http://travis-ci.org/robgolding63/tasklib
+ 
+ .. image:: https://coveralls.io/repos/robgolding63/tasklib/badge.png?branch=develop
+     :target: https://coveralls.io/r/robgolding63/tasklib?branch=develop
+ 
+ tasklib is a Python library for interacting with taskwarrior_ databases, using
+ a queryset API similar to that of Django's ORM.
+ 
+ Supports Python 2.6, 2.7, 3.2, 3.3 and 3.4 with taskwarrior 2.1.x and above.
+ Older versions of taskwarrior are untested and may not work.
+ 
+ Requirements
+ ------------
+ 
+ * taskwarrior_ v2.1.x or above.
+ 
+ Installation
+ ------------
+ 
+ Install via pip::
+ 
+     pip install tasklib
+ 
+ Usage
+ -----
+ 
+ tasklib has a similar API to that of Django's ORM::
+ 
+     >>> from tasklib import TaskWarrior
+ 
+     >>> tw = TaskWarrior('/home/rob/.task')
+     >>> tasks = tw.tasks.pending()
+     >>> tasks
+     ['Tidy the house', 'Learn German']
+     >>> tasks.filter(tags__contain='chores')
+     ['Tidy the house']
+     >>> type(tasks[0])
+     <class 'tasklib.task.Task'>
+     >>> tasks[0].done()
+     >>> tasks = tw.tasks.pending()
+     >>> tasks
+     ['Learn German']
+     >>> tasks[0]['tags'] = ['languages']
+     >>> tasks[0].save()
+ 
+ For more advanced usage, see the documentation_.
+ 
+ .. _taskwarrior: http://taskwarrior.org
+ .. _documentation: http://tasklib.readthedocs.org/en/latest/
 
--- /dev/null
+ # Makefile for Sphinx documentation
+ #
+ 
+ # You can set these variables from the command line.
+ SPHINXOPTS    =
+ SPHINXBUILD   = sphinx-build
+ PAPER         =
+ BUILDDIR      = _build
+ 
+ # User-friendly check for sphinx-build
+ ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+ $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+ endif
+ 
+ # Internal variables.
+ PAPEROPT_a4     = -D latex_paper_size=a4
+ PAPEROPT_letter = -D latex_paper_size=letter
+ ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ # the i18n builder cannot share the environment and doctrees with the others
+ I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ 
+ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+ 
+ help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html       to make standalone HTML files"
+       @echo "  dirhtml    to make HTML files named index.html in directories"
+       @echo "  singlehtml to make a single large HTML file"
+       @echo "  pickle     to make pickle files"
+       @echo "  json       to make JSON files"
+       @echo "  htmlhelp   to make HTML files and a HTML help project"
+       @echo "  qthelp     to make HTML files and a qthelp project"
+       @echo "  devhelp    to make HTML files and a Devhelp project"
+       @echo "  epub       to make an epub"
+       @echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+       @echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+       @echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+       @echo "  text       to make text files"
+       @echo "  man        to make manual pages"
+       @echo "  texinfo    to make Texinfo files"
+       @echo "  info       to make Texinfo files and run them through makeinfo"
+       @echo "  gettext    to make PO message catalogs"
+       @echo "  changes    to make an overview of all changed/added/deprecated items"
+       @echo "  xml        to make Docutils-native XML files"
+       @echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+       @echo "  linkcheck  to check all external links for integrity"
+       @echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+ 
+ clean:
+       rm -rf $(BUILDDIR)/*
+ 
+ html:
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+ 
+ dirhtml:
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+ 
+ singlehtml:
+       $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+       @echo
+       @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+ 
+ pickle:
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+ 
+ json:
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+ 
+ htmlhelp:
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in $(BUILDDIR)/htmlhelp."
+ 
+ qthelp:
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+             ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+       @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/tasklib.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/tasklib.qhc"
+ 
+ devhelp:
+       $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+       @echo
+       @echo "Build finished."
+       @echo "To view the help file:"
+       @echo "# mkdir -p $$HOME/.local/share/devhelp/tasklib"
+       @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/tasklib"
+       @echo "# devhelp"
+ 
+ epub:
+       $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+       @echo
+       @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+ 
+ latex:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+       @echo "Run \`make' in that directory to run these through (pdf)latex" \
+             "(use \`make latexpdf' here to do that automatically)."
+ 
+ latexpdf:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo "Running LaTeX files through pdflatex..."
+       $(MAKE) -C $(BUILDDIR)/latex all-pdf
+       @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+ 
+ latexpdfja:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo "Running LaTeX files through platex and dvipdfmx..."
+       $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+       @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+ 
+ text:
+       $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+       @echo
+       @echo "Build finished. The text files are in $(BUILDDIR)/text."
+ 
+ man:
+       $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+       @echo
+       @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+ 
+ texinfo:
+       $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+       @echo
+       @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+       @echo "Run \`make' in that directory to run these through makeinfo" \
+             "(use \`make info' here to do that automatically)."
+ 
+ info:
+       $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+       @echo "Running Texinfo files through makeinfo..."
+       make -C $(BUILDDIR)/texinfo info
+       @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+ 
+ gettext:
+       $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+       @echo
+       @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+ 
+ changes:
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+       @echo
+       @echo "The overview file is in $(BUILDDIR)/changes."
+ 
+ linkcheck:
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in $(BUILDDIR)/linkcheck/output.txt."
+ 
+ doctest:
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in $(BUILDDIR)/doctest/output.txt."
+ 
+ xml:
+       $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+       @echo
+       @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+ 
+ pseudoxml:
+       $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+       @echo
+       @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
 
--- /dev/null
+ # -*- coding: utf-8 -*-
+ #
+ # tasklib documentation build configuration file, created by
+ # sphinx-quickstart on Sun Nov 10 15:19:07 2013.
+ #
+ # This file is execfile()d with the current directory set to its
+ # containing dir.
+ #
+ # Note that not all possible configuration values are present in this
+ # autogenerated file.
+ #
+ # All configuration values have a default; values that are commented out
+ # serve to show the default.
+ 
+ import sys
+ import os
+ 
+ # If extensions (or modules to document with autodoc) are in another directory,
+ # add these directories to sys.path here. If the directory is relative to the
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
+ #sys.path.insert(0, os.path.abspath('.'))
+ 
+ # -- General configuration ------------------------------------------------
+ 
+ # If your documentation needs a minimal Sphinx version, state it here.
+ #needs_sphinx = '1.0'
+ 
+ # Add any Sphinx extension module names here, as strings. They can be
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+ # ones.
+ extensions = []
+ 
+ # Add any paths that contain templates here, relative to this directory.
+ templates_path = ['_templates']
+ 
+ # The suffix of source filenames.
+ source_suffix = '.rst'
+ 
+ # The encoding of source files.
+ #source_encoding = 'utf-8-sig'
+ 
+ # The master toctree document.
+ master_doc = 'index'
+ 
+ # General information about the project.
+ project = u'tasklib'
+ 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 = '1.2.1'
+ # The full version, including alpha/beta/rc tags.
+ release = '1.2.1'
+ 
+ # The language for content autogenerated by Sphinx. Refer to documentation
+ # for a list of supported languages.
+ #language = None
+ 
+ # There are two options for replacing |today|: either, you set today to some
+ # non-false value, then it is used:
+ #today = ''
+ # Else, today_fmt is used as the format for a strftime call.
+ #today_fmt = '%B %d, %Y'
+ 
+ # List of patterns, relative to source directory, that match files and
+ # directories to ignore when looking for source files.
+ exclude_patterns = ['_build']
+ 
+ # The reST default role (used for this markup: `text`) to use for all
+ # documents.
+ #default_role = None
+ 
+ # If true, '()' will be appended to :func: etc. cross-reference text.
+ #add_function_parentheses = True
+ 
+ # If true, the current module name will be prepended to all description
+ # unit titles (such as .. function::).
+ #add_module_names = True
+ 
+ # If true, sectionauthor and moduleauthor directives will be shown in the
+ # output. They are ignored by default.
+ #show_authors = False
+ 
+ # The name of the Pygments (syntax highlighting) style to use.
+ pygments_style = 'sphinx'
+ 
+ # A list of ignored prefixes for module index sorting.
+ #modindex_common_prefix = []
+ 
+ # If true, keep warnings as "system message" paragraphs in the built documents.
+ #keep_warnings = False
+ 
+ 
+ # -- Options for HTML output ----------------------------------------------
+ 
+ # The theme to use for HTML and HTML Help pages.  See the documentation for
+ # a list of builtin themes.
+ html_theme = 'default'
+ 
+ # Theme options are theme-specific and customize the look and feel of a theme
+ # further.  For a list of options available for each theme, see the
+ # documentation.
+ #html_theme_options = {}
+ 
+ # Add any paths that contain custom themes here, relative to this directory.
+ #html_theme_path = []
+ 
+ # The name for this set of Sphinx documents.  If None, it defaults to
+ # "<project> v<release> documentation".
+ #html_title = None
+ 
+ # A shorter title for the navigation bar.  Default is the same as html_title.
+ #html_short_title = None
+ 
+ # The name of an image file (relative to this directory) to place at the top
+ # of the sidebar.
+ #html_logo = None
+ 
+ # The name of an image file (within the static path) to use as favicon of the
+ # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+ # pixels large.
+ #html_favicon = None
+ 
+ # Add any paths that contain custom static files (such as style sheets) here,
+ # relative to this directory. They are copied after the builtin static files,
+ # so a file named "default.css" will overwrite the builtin "default.css".
+ html_static_path = ['_static']
+ 
+ # Add any extra paths that contain custom files (such as robots.txt or
+ # .htaccess) here, relative to this directory. These files are copied
+ # directly to the root of the documentation.
+ #html_extra_path = []
+ 
+ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+ # using the given strftime format.
+ #html_last_updated_fmt = '%b %d, %Y'
+ 
+ # If true, SmartyPants will be used to convert quotes and dashes to
+ # typographically correct entities.
+ #html_use_smartypants = True
+ 
+ # Custom sidebar templates, maps document names to template names.
+ #html_sidebars = {}
+ 
+ # Additional templates that should be rendered to pages, maps page names to
+ # template names.
+ #html_additional_pages = {}
+ 
+ # If false, no module index is generated.
+ #html_domain_indices = True
+ 
+ # If false, no index is generated.
+ #html_use_index = True
+ 
+ # If true, the index is split into individual pages for each letter.
+ #html_split_index = False
+ 
+ # If true, links to the reST sources are added to the pages.
+ #html_show_sourcelink = True
+ 
+ # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+ #html_show_sphinx = True
+ 
+ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+ #html_show_copyright = True
+ 
+ # If true, an OpenSearch description file will be output, and all pages will
+ # contain a <link> tag referring to it.  The value of this option must be the
+ # base URL from which the finished HTML is served.
+ #html_use_opensearch = ''
+ 
+ # This is the file name suffix for HTML files (e.g. ".xhtml").
+ #html_file_suffix = None
+ 
+ # Output file base name for HTML help builder.
+ htmlhelp_basename = 'tasklibdoc'
+ 
+ 
+ # -- Options for LaTeX output ---------------------------------------------
+ 
+ latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+ 
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+ 
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
+ }
+ 
+ # Grouping the document tree into LaTeX files. List of tuples
+ # (source start file, target name, title,
+ #  author, documentclass [howto, manual, or own class]).
+ latex_documents = [
+   ('index', 'tasklib.tex', u'tasklib Documentation',
+    u'Rob Golding', 'manual'),
+ ]
+ 
+ # The name of an image file (relative to this directory) to place at the top of
+ # the title page.
+ #latex_logo = None
+ 
+ # For "manual" documents, if this is true, then toplevel headings are parts,
+ # not chapters.
+ #latex_use_parts = False
+ 
+ # If true, show page references after internal links.
+ #latex_show_pagerefs = False
+ 
+ # If true, show URL addresses after external links.
+ #latex_show_urls = False
+ 
+ # Documents to append as an appendix to all manuals.
+ #latex_appendices = []
+ 
+ # If false, no module index is generated.
+ #latex_domain_indices = True
+ 
+ 
+ # -- Options for manual page output ---------------------------------------
+ 
+ # One entry per manual page. List of tuples
+ # (source start file, name, description, authors, manual section).
+ man_pages = [
+     ('index', 'tasklib', u'tasklib Documentation',
+      [u'Rob Golding'], 1)
+ ]
+ 
+ # If true, show URL addresses after external links.
+ #man_show_urls = False
+ 
+ 
+ # -- Options for Texinfo output -------------------------------------------
+ 
+ # Grouping the document tree into Texinfo files. List of tuples
+ # (source start file, target name, title, author,
+ #  dir menu entry, description, category)
+ texinfo_documents = [
+   ('index', 'tasklib', u'tasklib Documentation',
+    u'Rob Golding', 'tasklib', 'One line description of project.',
+    'Miscellaneous'),
+ ]
+ 
+ # Documents to append as an appendix to all manuals.
+ #texinfo_appendices = []
+ 
+ # If false, no module index is generated.
+ #texinfo_domain_indices = True
+ 
+ # How to display URL addresses: 'footnote', 'no', or 'inline'.
+ #texinfo_show_urls = 'footnote'
+ 
+ # If true, do not generate a @detailmenu in the "Top" node's menu.
+ #texinfo_no_detailmenu = False
 
--- /dev/null
+ Welcome to tasklib's documentation!
+ ===================================
+ 
+ tasklib is a Python library for interacting with taskwarrior_ databases, using
+ a queryset API similar to that of Django's ORM.
+ 
+ Supports Python 2.6, 2.7, 3.2, 3.3 and 3.4 with taskwarrior 2.1.x and above.
+ Older versions of taskwarrior are untested and may not work.
+ 
+ Requirements
+ ------------
+ 
+ * taskwarrior_ v2.1.x or above, although newest minor release is recommended.
+ 
+ Installation
+ ------------
+ 
+ Install via pip (recommended)::
+ 
+     pip install tasklib
+ 
+ Or clone from github::
+ 
+     git clone https://github.com/robgolding63/tasklib.git
+     cd tasklib
+     python setup.py install
+ 
+ Initialization
+ --------------
+ 
+ Optionally initialize the ``TaskWarrior`` instance with ``data_location`` (the
+ database directory). If it doesn't already exist, this will be created
+ automatically unless ``create=False``.
+ 
+ The default location is the same as taskwarrior's::
+ 
+     >>> tw = TaskWarrior(data_location='~/.task', create=True)
+ 
+ The ``TaskWarrior`` instance will also use your .taskrc configuration (so that
+ it recognizes the same UDAs as your task binary, uses the same configuration,
+ etc.). To override the location of the .taskrc, use
+ ``taskrc_location=~/some/different/path``.
+ 
+ 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(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> 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.
+ 
+ Task properties
+ ---------------
+ 
+ Tasklib defines several properties upon ``Task`` object, for convenience::
+ 
+     >>> t.save()
+     >>> t.saved
+     True
+     >>> t.pending
+     True
+     >>> t.active
+     False
+     >>> t.start()
+     >>> t.active
+     True
+     >>> t.done()
+     >>> t.completed
+     True
+     >>> t.pending
+     False
+     >>> t.delete()
+     >>> t.deleted
+     True
+ 
+ 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']
+ 
+ Tasks can also be started and stopped. Use ``start()`` and ``stop()``
+ respectively::
+ 
+     >>> task.start()
+     >>> task['start']
+     datetime.datetime(2015, 7, 16, 18, 48, 28, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
+     >>> task.stop()
+     >>> task['start']
+     >>> task.done()
+     >>> task['end']
+     datetime.datetime(2015, 7, 16, 18, 49, 2, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
+ 
+ 
+ Retrieving Tasks
+ ----------------
+ 
+ ``tw.tasks`` is a ``TaskQuerySet`` object which emulates the Django QuerySet
+ 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__contains=['work'])
+     ['Upgrade Ubuntu Server']
+ 
+ Filter arguments are passed to the ``task`` command (``__`` is replaced by
+ a period) so the above example is equivalent to the following command::
+ 
+     $ task status:pending tags.contain=work
+ 
+ Tasks can also be filtered using raw commands, like so::
+ 
+     >>> 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')
+     ['Upgrade Ubuntu Server']
+     >>> len(tw.tasks.completed())
+     227
+ 
+ Use ``get()`` to return the only task in a ``TaskQuerySet``, or raise an
+ exception::
+ 
+     >>> 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', '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 23! Lookup parameters were {'status': 'pending'}
+ 
+ Additionally, since filters return ``TaskQuerySets`` you can stack filters on top of each other::
+ 
+     >>> 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']
+ 
+ Equality of Task objects
+ ------------------------
+ 
+ Two Tasks are considered equal if they have the same UUIDs::
+ 
+     >>> task1 = Task(tw, description="Pet the dog")
+     >>> task1.save()
+     >>> task2 = tw.tasks.get(description="Pet the dog")
+     >>> task1 == task2
+     True
+ 
+ If you compare the two unsaved tasks, they are considered equal only if it's the
+ same Python object::
+ 
+     >>> task1 = Task(tw, description="Pet the cat")
+     >>> task2 = Task(tw, description="Pet the cat")
+     >>> task1 == task2
+     False
+     >>> task3 = task1
+     >>> task3 == task1
+     True
+ 
+ Accessing original values
+ -------------------------
+ 
+ To access the saved state of the Task, use dict-like access using the
+ ``original`` attribute:
+ 
+     >>> t = Task(tw, description="tidy up")
+     >>> t.save()
+     >>> t['description'] = "tidy up the kitchen and bathroom"
+     >>> t['description']
+     "tidy up the kitchen and bathroom"
+     >>> t.original['description']
+     "tidy up"
+ 
+ When you save the task, original values are refreshed to reflect the
+ saved state of the task:
+ 
+     >>> t.save()
+     >>> t.original['description']
+     "tidy up the kitchen and bathroom"
+ 
+ Dealing with dates and time
+ ---------------------------
+ 
+ Any timestamp-like attributes of the tasks are converted to timezone-aware
+ datetime objects. To achieve this, Tasklib leverages ``pytz`` Python module,
+ which brings the Olsen timezone databaze to Python.
+ 
+ This shields you from annoying details of Daylight Saving Time shifts
+ or conversion between different timezones. For example, to list all the
+ tasks which are due midnight if you're currently in Berlin:
+ 
+     >>> myzone = pytz.timezone('Europe/Berlin')
+     >>> midnight = myzone.localize(datetime(2015,2,2,0,0,0))
+     >>> tw.tasks.filter(due__before=midnight)
+ 
+ However, this is still a little bit tedious. That's why TaskWarrior object
+ is capable of automatic timezone detection, using the ``tzlocal`` Python
+ module. If your system timezone is set to 'Europe/Berlin', following example
+ will work the same way as the previous one:
+ 
+     >>> tw.tasks.filter(due__before=datetime(2015,2,2,0,0,0))
+ 
+ You can also use simple dates when filtering:
+ 
+     >>> tw.tasks.filter(due__before=date(2015,2,2))
+ 
+ In such case, a 00:00:00 is used as the time component.
+ 
+ Of course, you can use datetime naive objects when initializing Task object
+ or assigning values to datetime atrributes:
+ 
+     >>> t = Task(tw, description="Buy new shoes", due=date(2015,2,5))
+     >>> t['due']
+     datetime.datetime(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> t['due'] = date(2015,2,6,15,15,15)
+     >>> t['due']
+     datetime.datetime(2015, 2, 6, 15, 15, 15, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+ 
+ However, since timezone-aware and timezone-naive datetimes are not comparable
+ in Python, this can cause some unexpected behaviour:
+ 
+     >>> from datetime import datetime
+     >>> now = datetime.now()
+     >>> t = Task(tw, description="take out the trash now") 
+     >>> t['due'] = now
+     >>> now
+     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001)
+     >>> t['due']
+     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> t['due'] == now
+     Traceback (most recent call last):
+       File "<stdin>", line 1, in <module>
+       TypeError: can't compare offset-naive and offset-aware datetimes
+ 
+ If you want to compare datetime aware value with datetime naive value, you need
+ to localize the naive value first:
+ 
+     >>> from datetime import datetime
+     >>> from tasklib.task import local_zone
+     >>> now = local_zone.localize(datetime.now())
+     >>> t['due'] = now
+     >>> now
+     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> t['due'] == now
+     True
+ 
+ Also, note that it does not matter whether the timezone aware datetime objects
+ are set in the same timezone:
+ 
+     >>> import pytz
+     >>> t['due']
+     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> now.astimezone(pytz.utc)
+     datetime.datetime(2015, 2, 1, 18, 44, 4, 770001, tzinfo=<UTC>)
+     >>> t['due'] == now.astimezone(pytz.utc)
+     True
+ 
+ *Note*: Following behaviour is available only for TaskWarrior >= 2.4.0.
+ 
+ There is a third approach to setting up date time values, which leverages
+ the 'task calc' command. You can simply set any datetime attribute to
+ any string that contains an acceptable TaskWarrior-formatted time expression::
+ 
+     $ task calc now + 1d
+     2015-07-17T21:17:54
+ 
+ This syntax can be leveraged in the python interpreter as follows::
+ 
+     >>> t['due'] = "now + 1d"
+     >>> t['due']
+     datetime.datetime(2015, 7, 17, 21, 19, 31, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)
+ 
+ It can be easily seen that the string with TaskWarrior-formatted time expression
+ is automatically converted to native datetime in the local time zone.
+ 
+ For the list of acceptable formats and keywords, please consult:
+ 
+ * http://taskwarrior.org/docs/dates.html
+ * http://taskwarrior.org/docs/named_dates.html
+ 
+ However, as each such assigment involves call to 'task calc' for conversion,
+ it might cause some performance issues when assigning strings to datetime
+ attributes repeatedly, in a automated manner.
+ 
+ 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, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+     >>> 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.save()
+     >>> task.add_annotation("we can annotate saved tasks")
+     >>> task['annotations']
+     [we can annotate saved tasks]
+ 
+ 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
+ 
+ 
+ Additionally, you can use ``return_all=True`` flag, which returns
+ ``(stdout, sterr, return_code)`` triplet, and ``allow_failure=False``, which will
+ prevent tasklib from raising an exception if the task binary returned non-zero
+ return code::
+ 
+     >>> tw.execute_command(['invalidcommand'], allow_failure=False, return_all=True)
+     ([u''],
+      [u'Using alternate .taskrc file /home/tbabej/.taskrc',
+       u"[task next rc:/home/tbabej/.taskrc rc.recurrence.confirmation=no rc.json.array=off rc.confirmation=no rc.bulk=0 rc.dependency.confirmation=no description ~ 'invalidcommand']",
+       u'Configuration override rc.recurrence.confirmation:no',
+       u'Configuration override rc.json.array:off',
+       u'Configuration override rc.confirmation:no',
+       u'Configuration override rc.bulk:0',
+       u'Configuration override rc.dependency.confirmation:no',
+       u'No matches.',
+       u'There are local changes.  Sync required.'],
+      1)
+ 
+ 
+ Setting custom configuration values
+ -----------------------------------
+ 
+ By default, TaskWarrior uses configuration values stored in your .taskrc.
+ To see what configuration value overrides are passed to each executed
+ task command, have a peek into ``overrides`` attribute of ``TaskWarrior`` object::
+ 
+     >>> tw.overrides
+     {'confirmation': 'no', 'data.location': '/home/tbabej/.task'}
+ 
+ To pass your own configuration overrides, you just need to update this dictionary::
+ 
+     >>> tw.overrides.update({'hooks': 'off'})  # tasklib will not trigger hooks
+ 
+ Creating hook scripts
+ ---------------------
+ 
+ From version 2.4.0, TaskWarrior has support for hook scripts. Tasklib provides
+ some very useful helpers to write those. With tasklib, writing these becomes
+ a breeze::
+ 
+     #!/usr/bin/python
+ 
+     from tasklib.task import Task
+     task = Task.from_input()
+     # ... <custom logic>
+     print task.export_data()
+ 
+ For example, plugin which would assign the priority "H" to any task containing
+ three exclamation marks in the description, would go like this::
+ 
+     #!/usr/bin/python
+ 
+     from tasklib.task import Task
+     task = Task.from_input()
+ 
+     if "!!!" in task['description']:
+         task['priority'] = "H"
+ 
+     print task.export_data()
+ 
+ Tasklib can automatically detect whether it's running in the ``on-modify`` event,
+ which provides more input than ``on-add`` event and reads the data accordingly.
+ 
+ This means the example above works both for ``on-add`` and ``on-modify`` events!
+ 
+ Consenquently, you can create just one hook file for both ``on-add`` and
+ ``on-modify`` events, and you just need to create a symlink for the other one.
+ This removes the need for maintaining two copies of the same code base and/or
+ boilerplate code.
+ 
+ In ``on-modify`` events, tasklib loads both the original version and the modified
+ version of the task to the returned ``Task`` object. To access the original data
+ (in read-only manner), use ``original`` dict-like attribute:
+ 
+     >>> t = Task.from_input()
+     >>> t['description']
+     "Modified description"
+     >>> t.original['description']
+     "Original description"
+ 
+ Working with UDAs
+ -----------------
+ 
+ Since TaskWarrior does read your .taskrc, you need not to define any UDAs
+ in the TaskWarrior's config dictionary, as described above. Suppose we have
+ a estimate UDA in the .taskrc::
+ 
+     uda.estimate.type = numeric
+ 
+ We can simply filter and create tasks using the estimate UDA out of the box::
+ 
+     >>> tw = TaskWarrior()
+     >>> 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, ...}
+ 
+ We can also speficy UDAs as arguments in the TaskFilter::
+ 
+     >>> tw.tasks.filter(estimate=1000)
+     Long task
+ 
+ Syncing
+ -------
+ 
+ If you have configurated the needed config variables in your .taskrc, syncing
+ is as easy as::
+ 
+     >>> tw = TaskWarrior()
+     >>> tw.execute_command(['sync'])
+ 
+ If you want to use non-standard server/credentials, you'll need to provide configuration
+ overrides to the ``TaskWarrior`` instance. Update the ``config`` dictionary with the
+ values you desire to override, 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'])
+ 
+ 
+ .. _taskwarrior: http://taskwarrior.org
 
--- /dev/null
+ from setuptools import setup, find_packages
+ 
+ install_requirements = ['six>=1.4', 'pytz', 'tzlocal']
+ 
+ version = '1.2.1'
+ 
+ try:
+     import importlib
+ except ImportError:
+     install_requirements.append('importlib')
+ 
+ setup(
+     name='tasklib',
+     version=version,
+     description='Python Task Warrior library',
+     long_description=open('README.rst').read(),
+     author='Rob Golding',
+     author_email='rob@robgolding.com',
+     license='BSD',
+     url='https://github.com/robgolding63/tasklib',
+     download_url='https://github.com/robgolding63/tasklib/downloads',
+     packages=find_packages(),
+     include_package_data=True,
+     test_suite='tasklib.tests',
+     install_requires=install_requirements,
+     classifiers=[
+         'Development Status :: 4 - Beta',
+         'Programming Language :: Python',
+         "Programming Language :: Python :: 2",
+         "Programming Language :: Python :: 2.6",
+         "Programming Language :: Python :: 2.7",
+         "Programming Language :: Python :: 3",
+         "Programming Language :: Python :: 3.2",
+         "Programming Language :: Python :: 3.3",
+         'License :: OSI Approved :: BSD License',
+         'Topic :: Software Development :: Libraries :: Python Modules',
+         'Intended Audience :: Developers',
+     ],
+ )
 
--- /dev/null
+ from .backends import TaskWarrior
+ from .task import Task
+ from .serializing import local_zone
 
--- /dev/null
+ import abc
+ import copy
+ import datetime
+ import json
+ import logging
+ import os
+ import re
+ import six
+ import subprocess
+ 
+ from .task import Task, TaskQuerySet, ReadOnlyDictView
+ from .filters import TaskWarriorFilter
+ from .serializing import local_zone
+ 
+ DATE_FORMAT_CALC = '%Y-%m-%dT%H:%M:%S'
+ 
+ logger = logging.getLogger(__name__)
+ 
+ 
+ class Backend(object):
+ 
+     @abc.abstractproperty
+     def filter_class(self):
+         """Returns the TaskFilter class used by this backend"""
+         pass
+ 
+     @abc.abstractmethod
+     def filter_tasks(self, filter_obj):
+         """Returns a list of Task objects matching the given filter"""
+         pass
+ 
+     @abc.abstractmethod
+     def save_task(self, task):
+         pass
+ 
+     @abc.abstractmethod
+     def delete_task(self, task):
+         pass
+ 
+     @abc.abstractmethod
+     def start_task(self, task):
+         pass
+ 
+     @abc.abstractmethod
+     def stop_task(self, task):
+         pass
+ 
+     @abc.abstractmethod
+     def complete_task(self, task):
+         pass
+ 
+     @abc.abstractmethod
+     def refresh_task(self, task, after_save=False):
+         """
+         Refreshes the given task. Returns new data dict with serialized
+         attributes.
+         """
+         pass
+ 
+     @abc.abstractmethod
+     def annotate_task(self, task, annotation):
+         pass
+ 
+     @abc.abstractmethod
+     def denotate_task(self, task, annotation):
+         pass
+ 
+     @abc.abstractmethod
+     def sync(self):
+         """Syncs the backend database with the taskd server"""
+         pass
+ 
+     def convert_datetime_string(self, value):
+         """
+         Converts TW syntax datetime string to a localized datetime
+         object. This method is not mandatory.
+         """
+         raise NotImplementedError
+ 
+ 
+ class TaskWarriorException(Exception):
+     pass
+ 
+ 
+ class TaskWarrior(Backend):
+ 
+     VERSION_2_1_0 = six.u('2.1.0')
+     VERSION_2_2_0 = six.u('2.2.0')
+     VERSION_2_3_0 = six.u('2.3.0')
+     VERSION_2_4_0 = six.u('2.4.0')
+     VERSION_2_4_1 = six.u('2.4.1')
+     VERSION_2_4_2 = six.u('2.4.2')
+     VERSION_2_4_3 = six.u('2.4.3')
+     VERSION_2_4_4 = six.u('2.4.4')
+     VERSION_2_4_5 = six.u('2.4.5')
+ 
+     def __init__(self, data_location=None, create=True,
+                  taskrc_location=None, task_command='task',
+                  version_override=None):
+         self.taskrc_location = None
+         if taskrc_location:
+             self.taskrc_location = os.path.expanduser(taskrc_location)
+ 
+             # If taskrc does not exist, pass / to use defaults and avoid creating
+             # dummy .taskrc file by TaskWarrior
+             if not os.path.exists(self.taskrc_location):
+                 self.taskrc_location = '/'
+ 
+         self.task_command = task_command
+ 
+         self._config = None
+         self.version = version_override or self._get_version()
+         self.overrides = {
+             'confirmation': 'no',
+             'dependency.confirmation': 'no',  # See TW-1483 or taskrc man page
+             'recurrence.confirmation': 'no',  # Necessary for modifying R tasks
+ 
+             # Defaults to on since 2.4.5, we expect off during parsing
+             'json.array': 'off',
+ 
+             # 2.4.3 onwards supports 0 as infite bulk, otherwise set just
+             # arbitrary big number which is likely to be large enough
+             'bulk': 0 if self.version >= self.VERSION_2_4_3 else 100000,
+         }
+ 
+         # Set data.location override if passed via kwarg
+         if data_location is not None:
+             data_location = os.path.expanduser(data_location)
+             if create and not os.path.exists(data_location):
+                 os.makedirs(data_location)
+             self.overrides['data.location'] = data_location
+ 
+         self.tasks = TaskQuerySet(self)
+ 
+     def _get_task_command(self):
+         return self.task_command.split()
+ 
+     def _get_command_args(self, args, config_override=None):
+         command_args = self._get_task_command()
+         overrides = self.overrides.copy()
+         overrides.update(config_override or dict())
+         for item in overrides.items():
+             command_args.append('rc.{0}={1}'.format(*item))
+         command_args.extend([
+             x.decode('utf-8') if isinstance(x, six.binary_type)
+             else six.text_type(x) for x in args
+         ])
+         return command_args
+ 
+     def _get_version(self):
+         p = subprocess.Popen(
+             self._get_task_command() + ['--version'],
+             stdout=subprocess.PIPE,
+             stderr=subprocess.PIPE)
+         stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
+         return stdout.strip('\n')
+ 
+     def _get_modified_task_fields_as_args(self, task):
+         args = []
+ 
+         def add_field(field):
+             # Add the output of format_field method to args list (defaults to
+             # field:value)
+             serialized_value = task._serialize(field, task._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 = six.u("'{0}'").format(
+                     serialized_value)
+ 
+             format_default = lambda task: six.u("{0}:{1}").format(
+                 field, escaped_serialized_value)
+ 
+             format_func = getattr(self, 'format_{0}'.format(field),
+                                   format_default)
+ 
+             args.append(format_func(task))
+ 
+         # If we're modifying saved task, simply pass on all modified fields
+         if task.saved:
+             for field in task._modified_fields:
+                 add_field(field)
+ 
+         # For new tasks, pass all fields that make sense
+         else:
+             for field in task._data.keys():
+                 # We cannot set stuff that's read only (ID, UUID, ..)
+                 if field in task.read_only_fields:
+                     continue
+                 # We do not want to do field deletion for new tasks
+                 if task._data[field] is None:
+                     continue
+                 # Otherwise we're fine
+                 add_field(field)
+ 
+         return args
+ 
+     def format_depends(self, task):
+         # We need to generate added and removed dependencies list,
+         # since Taskwarrior does not accept redefining dependencies.
+ 
+         # 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
+ 
+         old_dependencies = task._original_data.get('depends', set())
+ 
+         added = task['depends'] - old_dependencies
+         removed = old_dependencies - task['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):
+         # Task version older than 2.4.0 ignores first word of the
+         # task description if description: prefix is used
+         if self.version < self.VERSION_2_4_0:
+             return task._data['description']
+         else:
+             return six.u("description:'{0}'").format(
+                 task._data['description'] or '',
+             )
+ 
+     def convert_datetime_string(self, value):
+ 
+         if self.version >= self.VERSION_2_4_0:
+             # For strings, use 'calc' to evaluate the string to datetime
+             # available since TW 2.4.0
+             args = value.split()
+             result = self.execute_command(['calc'] + args)
+             naive = datetime.datetime.strptime(result[0], DATE_FORMAT_CALC)
+             localized = local_zone.localize(naive)
+         else:
+             raise ValueError(
+                 'Provided value could not be converted to '
+                 'datetime, its type is not supported: {}'
+                 .format(type(value)),
+             )
+ 
+         return localized
+ 
+     @property
+     def filter_class(self):
+         return TaskWarriorFilter
+ 
+     # Public interface
+ 
+     @property
+     def config(self):
+         # First, check if memoized information is available
+         if self._config:
+             return self._config
+ 
+         # If not, fetch the config using the 'show' command
+         raw_output = self.execute_command(
+             ['show'],
+             config_override={'verbose': 'nothing'}
+         )
+ 
+         config = dict()
+         config_regex = re.compile(r'^(?P<key>[^\s]+)\s+(?P<value>[^\s].*$)')
+ 
+         for line in raw_output:
+             match = config_regex.match(line)
+             if match:
+                 config[match.group('key')] = match.group('value').strip()
+ 
+         # Memoize the config dict
+         self._config = ReadOnlyDictView(config)
+ 
+         return self._config
+ 
+     def execute_command(self, args, config_override=None, allow_failure=True,
+                         return_all=False):
+         command_args = self._get_command_args(
+             args, config_override=config_override)
+         logger.debug(u' '.join(command_args))
+ 
+         env = os.environ.copy()
+         if self.taskrc_location:
+             env['TASKRC'] = self.taskrc_location
+         p = subprocess.Popen(command_args, stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE, env=env)
+         stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
+         if p.returncode and allow_failure:
+             if stderr.strip():
+                 error_msg = stderr.strip()
+             else:
+                 error_msg = stdout.strip()
+             error_msg += u'\nCommand used: ' + u' '.join(command_args)
+             raise TaskWarriorException(error_msg)
+ 
+         # Return all whole triplet only if explicitly asked for
+         if not return_all:
+             return stdout.rstrip().split('\n')
+         else:
+             return (stdout.rstrip().split('\n'),
+                     stderr.rstrip().split('\n'),
+                     p.returncode)
+ 
+     def enforce_recurrence(self):
+         # Run arbitrary report command which will trigger generation
+         # of recurrent tasks.
+ 
+         # Only necessary for TW up to 2.4.1, fixed in 2.4.2.
+         if self.version < self.VERSION_2_4_2:
+             self.execute_command(['next'], allow_failure=False)
+ 
+     def merge_with(self, path, push=False):
+         path = path.rstrip('/') + '/'
+         self.execute_command(['merge', path], config_override={
+             'merge.autopush': 'yes' if push else 'no',
+         })
+ 
+     def undo(self):
+         self.execute_command(['undo'])
+ 
+     # Backend interface implementation
+ 
+     def filter_tasks(self, filter_obj):
+         self.enforce_recurrence()
+         args = ['export'] + filter_obj.get_filter_params()
+         tasks = []
+         for line in self.execute_command(args):
+             if line:
+                 data = line.strip(',')
+                 try:
+                     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 save_task(self, task):
+         """Save a task into TaskWarrior database using add/modify call"""
+ 
+         args = [task['uuid'], 'modify'] if task.saved else ['add']
+         args.extend(self._get_modified_task_fields_as_args(task))
+         output = self.execute_command(args)
+ 
+         # Parse out the new ID, if the task is being added for the first time
+         if not task.saved:
+             id_lines = [l for l in output if l.startswith('Created task ')]
+ 
+             # Complain loudly if it seems that more tasks were created
+             # Should not happen.
+             # Expected output: Created task 1.
+             #                  Created task 1 (recurrence template).
+             if len(id_lines) != 1 or len(id_lines[0].split(' ')) not in (3, 5):
+                 raise TaskWarriorException(
+                     'Unexpected output when creating '
+                     'task: %s' % '\n'.join(id_lines),
+                 )
+ 
+             # Circumvent the ID storage, since ID is considered read-only
+             identifier = id_lines[0].split(' ')[2].rstrip('.')
+ 
+             # Identifier can be either ID or UUID for completed tasks
+             try:
+                 task._data['id'] = int(identifier)
+             except ValueError:
+                 task._data['uuid'] = identifier
+ 
+         # 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
+         task.refresh(after_save=True)
+ 
+     def delete_task(self, task):
+         self.execute_command([task['uuid'], 'delete'])
+ 
+     def start_task(self, task):
+         self.execute_command([task['uuid'], 'start'])
+ 
+     def stop_task(self, task):
+         self.execute_command([task['uuid'], 'stop'])
+ 
+     def complete_task(self, task):
+         # Older versions of TW do not stop active task at completion
+         if self.version < self.VERSION_2_4_0 and task.active:
+             task.stop()
+ 
+         self.execute_command([task['uuid'], 'done'])
+ 
+     def annotate_task(self, task, annotation):
+         args = [task['uuid'], 'annotate', annotation]
+         self.execute_command(args)
+ 
+     def denotate_task(self, task, annotation):
+         args = [task['uuid'], 'denotate', annotation]
+         self.execute_command(args)
+ 
+     def refresh_task(self, task, after_save=False):
+         # We need to use ID as backup for uuid here for the refreshes
+         # of newly saved tasks. Any other place in the code is fine
+         # with using UUID only.
+         args = [task['uuid'] or task['id'], 'export']
+         output = self.execute_command(args)
+ 
+         def valid(output):
+             return len(output) == 1 and output[0].startswith('{')
+ 
+         # For older TW versions attempt to uniquely locate the task
+         # using the data we have if it has been just saved.
+         # This can happen when adding a completed task on older TW versions.
+         if (not valid(output) and self.version < self.VERSION_2_4_5
+                 and after_save):
+ 
+             # Make a copy, removing ID and UUID. It's most likely invalid
+             # (ID 0) if it failed to match a unique task.
+             data = copy.deepcopy(task._data)
+             data.pop('id', None)
+             data.pop('uuid', None)
+ 
+             taskfilter = self.filter_class(self)
+             for key, value in data.items():
+                 taskfilter.add_filter_param(key, value)
+ 
+             output = self.execute_command(['export'] +
+                                           taskfilter.get_filter_params())
+ 
+         # If more than 1 task has been matched still, raise an exception
+         if not valid(output):
+             raise TaskWarriorException(
+                 'Unique identifiers {0} with description: {1} matches '
+                 'multiple tasks: {2}'.format(
+                     task['uuid'] or task['id'], task['description'], output)
+             )
+ 
+         return json.loads(output[0])
+ 
+     def sync(self):
+         self.execute_command(['sync'])
 
--- /dev/null
+ import abc
+ import six
+ from .serializing import SerializingObject
+ 
+ 
+ class TaskFilter(object):
+     """
+     Abstract base class that defines interface of a TaskFilter.
+     """
+ 
+     @abc.abstractmethod
+     def add_filter(self, arg):
+         """
+         Processes an non-keyword filter.
+         """
+         pass
+ 
+     @abc.abstractmethod
+     def add_filter_param(self, key, value):
+         """
+         Processes a keyword filter.
+         """
+         pass
+ 
+     @abc.abstractmethod
+     def clone(self):
+         """
+         Returns a new deep copy of itself.
+         """
+         pass
+ 
+ 
+ class TaskWarriorFilter(TaskFilter, SerializingObject):
+     """
+     A set of parameters to filter the task list with.
+     """
+ 
+     def __init__(self, backend, filter_params=None):
+         self.filter_params = filter_params or []
+         super(TaskFilter, self).__init__(backend)
+ 
+     def add_filter(self, filter_str):
+         self.filter_params.append(filter_str)
+ 
+     def add_filter_param(self, key, value):
+         key = key.replace('__', '.')
+ 
+         # Replace the value with empty string, since that is the
+         # convention in TW for empty values
+         attribute_key = key.split('.')[0]
+ 
+         # Since this is user input, we need to normalize before we serialize
+         value = self._normalize(attribute_key, value)
+         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:
+             # 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
+             # which is, however, fixed in 2.4.5
+             if self.backend.version < self.backend.VERSION_2_4_5:
+                 modifier = '.is' if value else '.none'
+                 key = key + modifier if '.' not in key else key
+ 
+             self.filter_params.append(six.u("{0}:{1}").format(key, value))
+ 
+     def get_filter_params(self):
+         return [f for f in self.filter_params if f]
+ 
+     def clone(self):
+         c = self.__class__(self.backend)
+         c.filter_params = list(self.filter_params)
+         return c
 
--- /dev/null
+ """
+ Provides lazy implementations for Task and TaskQuerySet.
+ """
+ 
+ 
+ class LazyUUIDTask(object):
+     """
+     A lazy wrapper around Task object, referenced by UUID.
+ 
+     - Supports comparison with LazyUUIDTask or Task objects (equality by UUIDs)
+     - If any attribute other than 'uuid' requested, a lookup in the
+       backend will be performed and this object will be replaced by a proper
+       Task object.
+     """
+ 
+     def __init__(self, tw, uuid):
+         self._tw = tw
+         self._uuid = uuid
+ 
+     def __getitem__(self, key):
+         # LazyUUIDTask does not provide anything else other than 'uuid'
+         if key is 'uuid':
+             return self._uuid
+         else:
+             self.replace()
+             return self[key]
+ 
+     def __getattr__(self, name):
+         # Getattr is called only if the attribute could not be found using
+         # normal means
+         self.replace()
+         return getattr(self, name)
+ 
+     def __eq__(self, other):
+         if other and other['uuid']:
+             # For saved Tasks, just define equality by equality of uuids
+             return self['uuid'] == other['uuid']
+ 
+     def __ne__(self, other):
+         return not self.__eq__(other)
+ 
+     def __hash__(self):
+         return self['uuid'].__hash__()
+ 
+     def __repr__(self):
+         return 'LazyUUIDTask: {0}'.format(self._uuid)
+ 
+     def __copy__(self):
+         return LazyUUIDTask(self._tw, self._uuid)
+ 
+     def __deepcopy__(self, memo):
+         return LazyUUIDTask(self._tw, self._uuid)
+ 
+     @property
+     def saved(self):
+         """
+         Implementation of the 'saved' property. Always returns True.
+         """
+         return True
+ 
+     @property
+     def _modified_fields(self):
+         return set()
+ 
+     @property
+     def modified(self):
+         return False
+ 
+     def replace(self):
+         """
+         Performs conversion to the regular Task object, referenced by the
+         stored UUID.
+         """
+ 
+         replacement = self._tw.tasks.get(uuid=self._uuid)
+         self.__class__ = replacement.__class__
+         self.__dict__ = replacement.__dict__
+ 
+ 
+ class LazyUUIDTaskSet(object):
+     """
+     A lazy wrapper around TaskQuerySet object, for tasks referenced by UUID.
+ 
+     - Supports 'in' operator with LazyUUIDTask or Task objects
+     - If iteration over the objects in the LazyUUIDTaskSet is requested, the
+       LazyUUIDTaskSet will be converted to QuerySet and evaluated
+     """
+ 
+     def __init__(self, tw, uuids):
+         self._tw = tw
+         self._uuids = set(uuids)
+ 
+     def __getattr__(self, name):
+         # Getattr is called only if the attribute could not be found using
+         # normal means
+ 
+         if name.startswith('__'):
+             # If some internal method was being search, do not convert
+             # to TaskQuerySet just because of that
+             raise AttributeError
+         else:
+             self.replace()
+             return getattr(self, name)
+ 
+     def __repr__(self):
+         return 'LazyUUIDTaskSet([{0}])'.format(', '.join(self._uuids))
+ 
+     def __eq__(self, other):
+         return (set(t['uuid'] for t in other) if other else set()) == self._uuids
+ 
+     def __ne__(self, other):
+         return not (self == other)
+ 
+     def __contains__(self, task):
+         return task['uuid'] in self._uuids
+ 
+     def __len__(self):
+         return len(self._uuids)
+ 
+     def __iter__(self):
+         for uuid in self._uuids:
+             yield LazyUUIDTask(self._tw, uuid)
+ 
+     def __sub__(self, other):
+         return self.difference(other)
+ 
+     def __isub__(self, other):
+         return self.difference_update(other)
+ 
+     def __rsub__(self, other):
+         return LazyUUIDTaskSet(
+             self._tw,
+             set(t['uuid'] for t in other) - self._uuids,
+         )
+ 
+     def __or__(self, other):
+         return self.union(other)
+ 
+     def __ior__(self, other):
+         return self.update(other)
+ 
+     def __ror__(self, other):
+         return self.union(other)
+ 
+     def __xor__(self, other):
+         return self.symmetric_difference(other)
+ 
+     def __ixor__(self, other):
+         return self.symmetric_difference_update(other)
+ 
+     def __rxor__(self, other):
+         return self.symmetric_difference(other)
+ 
+     def __and__(self, other):
+         return self.intersection(other)
+ 
+     def __iand__(self, other):
+         return self.intersection_update(other)
+ 
+     def __rand__(self, other):
+         return self.intersection(other)
+ 
+     def __le__(self, other):
+         return self.issubset(other)
+ 
+     def __ge__(self, other):
+         return self.issuperset(other)
+ 
+     def issubset(self, other):
+         return all([task in other for task in self])
+ 
+     def issuperset(self, other):
+         return all([task in self for task in other])
+ 
+     def union(self, other):
+         return LazyUUIDTaskSet(
+             self._tw,
+             self._uuids | set(t['uuid'] for t in other),
+         )
+ 
+     def intersection(self, other):
+         return LazyUUIDTaskSet(
+             self._tw,
+             self._uuids & set(t['uuid'] for t in other),
+         )
+ 
+     def difference(self, other):
+         return LazyUUIDTaskSet(
+             self._tw,
+             self._uuids - set(t['uuid'] for t in other),
+         )
+ 
+     def symmetric_difference(self, other):
+         return LazyUUIDTaskSet(
+             self._tw,
+             self._uuids ^ set(t['uuid'] for t in other),
+         )
+ 
+     def update(self, other):
+         self._uuids |= set(t['uuid'] for t in other)
+         return self
+ 
+     def intersection_update(self, other):
+         self._uuids &= set(t['uuid'] for t in other)
+         return self
+ 
+     def difference_update(self, other):
+         self._uuids -= set(t['uuid'] for t in other)
+         return self
+ 
+     def symmetric_difference_update(self, other):
+         self._uuids ^= set(t['uuid'] for t in other)
+         return self
+ 
+     def add(self, task):
+         self._uuids.add(task['uuid'])
+ 
+     def remove(self, task):
+         self._uuids.remove(task['uuid'])
+ 
+     def pop(self):
+         return self._uuids.pop()
+ 
+     def clear(self):
+         self._uuids.clear()
+ 
+     def replace(self):
+         """
+         Performs conversion to the regular TaskQuerySet object, referenced by
+         the stored UUIDs.
+         """
+ 
+         replacement = self._tw.tasks.filter(' '.join(self._uuids))
+         self.__class__ = replacement.__class__
+         self.__dict__ = replacement.__dict__
 
--- /dev/null
+ import datetime
+ import importlib
+ import json
+ import pytz
+ import six
+ import tzlocal
+ 
+ 
+ from .lazy import LazyUUIDTaskSet, LazyUUIDTask
+ 
+ DATE_FORMAT = '%Y%m%dT%H%M%SZ'
+ local_zone = tzlocal.get_localzone()
+ 
+ 
+ class SerializingObject(object):
+     """
+     Common ancestor for TaskResource & TaskWarriorFilter, 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.
+ 
+     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 __init__(self, backend):
+         self.backend = backend
+ 
+     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 _normalize(self, key, value):
+         """
+         Use normalize_<key> methods to normalize user input. Any user
+         input will be normalized at the moment it is used as filter,
+         or entered as a value of Task attribute.
+         """
+ 
+         # None value should not be converted by normalizer
+         if value is None:
+             return None
+ 
+         normalize_func = getattr(self, 'normalize_{0}'.format(key),
+                                  lambda x: x)
+ 
+         return normalize_func(value)
+ 
+     def timestamp_serializer(self, date):
+         if not date:
+             return ''
+ 
+         # Any serialized timestamp should be localized, we need to
+         # convert to UTC before converting to string (DATE_FORMAT uses UTC)
+         date = date.astimezone(pytz.utc)
+ 
+         return date.strftime(DATE_FORMAT)
+ 
+     def timestamp_deserializer(self, date_str):
+         if not date_str:
+             return None
+ 
+         # Return timestamp localized in the local zone
+         naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
+         localized_timestamp = pytz.utc.localize(naive_timestamp)
+         return localized_timestamp.astimezone(local_zone)
+ 
+     def serialize_entry(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_entry(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_entry(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_modified(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_modified(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_modified(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_start(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_start(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_start(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_end(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_end(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_end(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_due(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_due(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_due(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_scheduled(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_scheduled(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_scheduled(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_until(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_until(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_until(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_wait(self, value):
+         return self.timestamp_serializer(value)
+ 
+     def deserialize_wait(self, value):
+         return self.timestamp_deserializer(value)
+ 
+     def normalize_wait(self, value):
+         return self.datetime_normalizer(value)
+ 
+     def serialize_annotations(self, value):
+         value = value if value is not None else []
+ 
+         # This may seem weird, but it's correct, we want to export
+         # a list of dicts as serialized value
+         serialized_annotations = [json.loads(annotation.export_data())
+                                   for annotation in value]
+         return serialized_annotations if serialized_annotations else ''
+ 
+     def deserialize_annotations(self, data):
+         task_module = importlib.import_module('tasklib.task')
+         TaskAnnotation = getattr(task_module, 'TaskAnnotation')
+         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, six.string_types):
+             return set(tags.split(',')) if tags else set()
+         return set(tags or [])
+ 
+     def serialize_parent(self, parent):
+         return parent['uuid'] if parent else ''
+ 
+     def deserialize_parent(self, uuid):
+         return LazyUUIDTask(self.backend, uuid) if uuid else None
+ 
+     def serialize_depends(self, value):
+         # Return the list of uuids
+         value = value if value is not None else set()
+ 
+         if isinstance(value, LazyUUIDTaskSet):
+             return ','.join(value._uuids)
+         else:
+             return ','.join(task['uuid'] for task in value)
+ 
+     def deserialize_depends(self, raw_uuids):
+         raw_uuids = raw_uuids or []  # Convert None to empty list
+ 
+         if not raw_uuids:
+             return set()
+ 
+         # TW 2.4.4 encodes list of dependencies as a single string
+         if type(raw_uuids) is not list:
+             uuids = raw_uuids.split(',')
+         # TW 2.4.5 and later exports them as a list, no conversion needed
+         else:
+             uuids = raw_uuids
+ 
+         return LazyUUIDTaskSet(self.backend, uuids)
+ 
+     def datetime_normalizer(self, value):
+         """
+         Normalizes date/datetime value (considered to come from user input)
+         to localized datetime value. Following conversions happen:
+ 
+         naive date -> localized datetime with the same date, and time=midnight
+         naive datetime -> localized datetime with the same value
+         localized datetime -> localized datetime (no conversion)
+         """
+ 
+         if (
+             isinstance(value, datetime.date)
+             and not isinstance(value, datetime.datetime)
+         ):
+             # Convert to local midnight
+             value_full = datetime.datetime.combine(value, datetime.time.min)
+             localized = local_zone.localize(value_full)
+         elif isinstance(value, datetime.datetime):
+             if value.tzinfo is None:
+                 # Convert to localized datetime object
+                 localized = local_zone.localize(value)
+             else:
+                 # If the value is already localized, there is no need to change
+                 # time zone at this point. Also None is a valid value too.
+                 localized = value
+         elif isinstance(value, six.string_types):
+             localized = self.backend.convert_datetime_string(value)
+         else:
+             raise ValueError("Provided value could not be converted to "
+                              "datetime, its type is not supported: {}"
+                              .format(type(value)))
+ 
+         return localized
+ 
+     def normalize_uuid(self, value):
+         # Enforce sane UUID
+         if not isinstance(value, six.string_types) or value == '':
+             raise ValueError("UUID must be a valid non-empty string, "
+                              "not: {}".format(value))
+ 
+         return value
 
--- /dev/null
+ from __future__ import print_function
+ import copy
+ import importlib
+ import json
+ import logging
+ import os
+ import six
+ import sys
+ 
+ from .serializing import SerializingObject
+ 
+ DATE_FORMAT = '%Y%m%dT%H%M%SZ'
+ REPR_OUTPUT_SIZE = 10
+ PENDING = 'pending'
+ COMPLETED = 'completed'
+ DELETED = 'deleted'
+ WAITING = 'waiting'
+ RECURRING = 'recurring'
+ 
+ logger = logging.getLogger(__name__)
+ 
+ 
+ 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 __unicode__(self):
+         return six.u('ReadOnlyDictView: {0}'.format(repr(self.viewed_dict)))
+ 
+     __repr__ = __unicode__
+ 
+     def get(self, key, default=None):
+         return copy.deepcopy(self.viewed_dict.get(key, default))
+ 
+     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 TaskResource(SerializingObject):
+     read_only_fields = []
+ 
+     def _load_data(self, data):
+         self._data = dict((key, self._deserialize(key, value))
+                           for key, value in data.items())
+         # We need to use a copy for original data, so that changes
+         # are not propagated.
+         self._original_data = copy.deepcopy(self._data)
+ 
+     def _update_data(self, data, update_original=False, remove_missing=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(dict((key, self._deserialize(key, value))
+                                for key, value in data.items()))
+ 
+         # In certain situations, we want to treat missing keys as removals
+         if remove_missing:
+             for key in set(self._data.keys()) - set(data.keys()):
+                 self._data[key] = None
+ 
+         if update_original:
+             self._original_data = copy.deepcopy(self._data)
+ 
+     def __getitem__(self, key):
+         # This is a workaround to make TaskResource non-iterable
+         # over simple index-based iteration
+         try:
+             int(key)
+             raise StopIteration
+         except ValueError:
+             pass
+ 
+         if key not in self._data:
+             self._data[key] = self._deserialize(key, None)
+ 
+         return self._data.get(key)
+ 
+     def __setitem__(self, key, value):
+         if key in self.read_only_fields:
+             raise RuntimeError('Field \'%s\' is read-only' % key)
+ 
+         # Normalize the user input before saving it
+         value = self._normalize(key, value)
+         self._data[key] = value
+ 
+     def __str__(self):
+         s = six.text_type(self.__unicode__())
+         if not six.PY3:
+             s = s.encode('utf-8')
+         return s
+ 
+     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']
+ 
+     def __init__(self, task, data=None):
+         self.task = task
+         self._load_data(data or dict())
+         super(TaskAnnotation, self).__init__(task.backend)
+ 
+     def remove(self):
+         self.task.remove_annotation(self)
+ 
+     def __unicode__(self):
+         return self['description']
+ 
+     def __eq__(self, other):
+         # consider 2 annotations equal if they belong to the same task, and
+         # their data dics are the same
+         return self.task == other.task and self._data == other._data
+ 
+     def __ne__(self, other):
+         return not self.__eq__(other)
+ 
+     __repr__ = __unicode__
+ 
+ 
+ class Task(TaskResource):
+     read_only_fields = ['id', 'entry', 'urgency', 'uuid', 'modified']
+ 
+     class DoesNotExist(Exception):
+         pass
+ 
+     class CompletedTask(Exception):
+         """
+         Raised when the operation cannot be performed on the completed task.
+         """
+         pass
+ 
+     class DeletedTask(Exception):
+         """
+         Raised when the operation cannot be performed on the deleted task.
+         """
+         pass
+ 
+     class ActiveTask(Exception):
+         """
+         Raised when the operation cannot be performed on the active task.
+         """
+         pass
+ 
+     class InactiveTask(Exception):
+         """
+         Raised when the operation cannot be performed on an inactive task.
+         """
+         pass
+ 
+     class NotSaved(Exception):
+         """
+         Raised when the operation cannot be performed on the task, because
+         it has not been saved to TaskWarrior yet.
+         """
+         pass
+ 
+     @classmethod
+     def from_input(cls, input_file=sys.stdin, modify=None, backend=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.
+         """
+ 
+         # 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
+ 
+         # Create the TaskWarrior instance if none passed
+         if backend is None:
+             backends = importlib.import_module('tasklib.backends')
+             hook_parent_dir = os.path.dirname(os.path.dirname(sys.argv[0]))
+             backend = backends.TaskWarrior(data_location=hook_parent_dir)
+ 
+         # TaskWarrior instance is set to None
+         task = cls(backend)
+ 
+         # 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()),
+                               remove_missing=True)
+ 
+         return task
+ 
+     def __init__(self, backend, **kwargs):
+         super(Task, self).__init__(backend)
+ 
+         # 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)
+ 
+         # 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._data = dict((key, self._normalize(key, value))
+                           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']
+ 
+     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 __ne__(self, other):
+         return not self.__eq__(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 completed(self):
+         return self['status'] == six.text_type('completed')
+ 
+     @property
+     def deleted(self):
+         return self['status'] == six.text_type('deleted')
+ 
+     @property
+     def waiting(self):
+         return self['status'] == six.text_type('waiting')
+ 
+     @property
+     def pending(self):
+         return self['status'] == six.text_type('pending')
+ 
+     @property
+     def recurring(self):
+         return self['status'] == six.text_type('recurring')
+ 
+     @property
+     def active(self):
+         return self['start'] is not None
+ 
+     @property
+     def saved(self):
+         return self['uuid'] is not None or self['id'] is not None
+ 
+     def serialize_depends(self, cur_dependencies):
+         # Check that all the tasks are saved
+         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,
+                 )
+ 
+         return super(Task, self).serialize_depends(cur_dependencies)
+ 
+     def delete(self):
+         if not self.saved:
+             raise Task.NotSaved(
+                 'Task needs to be saved before it can be deleted',
+             )
+ 
+         # Refresh the status, and raise exception if the task is deleted
+         self.refresh(only_fields=['status'])
+ 
+         if self.deleted:
+             raise Task.DeletedTask('Task was already deleted')
+ 
+         self.backend.delete_task(self)
+ 
+         # Refresh the status again, so that we have updated info stored
+         self.refresh(only_fields=['status', 'start', 'end'])
+ 
+     def start(self):
+         if not self.saved:
+             raise Task.NotSaved(
+                 'Task needs to be saved before it can be started',
+             )
+ 
+         # Refresh, and raise exception if task is already completed/deleted
+         self.refresh(only_fields=['status'])
+ 
+         if self.completed:
+             raise Task.CompletedTask('Cannot start a completed task')
+         elif self.deleted:
+             raise Task.DeletedTask('Deleted task cannot be started')
+         elif self.active:
+             raise Task.ActiveTask('Task is already active')
+ 
+         self.backend.start_task(self)
+ 
+         # Refresh the status again, so that we have updated info stored
+         self.refresh(only_fields=['status', 'start'])
+ 
+     def stop(self):
+         if not self.saved:
+             raise Task.NotSaved(
+                 'Task needs to be saved before it can be stopped',
+             )
+ 
+         # Refresh, and raise exception if task is already completed/deleted
+         self.refresh(only_fields=['status'])
+ 
+         if not self.active:
+             raise Task.InactiveTask('Cannot stop an inactive task')
+ 
+         self.backend.stop_task(self)
+ 
+         # Refresh the status again, so that we have updated info stored
+         self.refresh(only_fields=['status', 'start'])
+ 
+     def done(self):
+         if not self.saved:
+             raise Task.NotSaved(
+                 'Task needs to be saved before it can be completed',
+             )
+ 
+         # Refresh, and raise exception if task is already completed/deleted
+         self.refresh(only_fields=['status'])
+ 
+         if self.completed:
+             raise Task.CompletedTask('Cannot complete a completed task')
+         elif self.deleted:
+             raise Task.DeletedTask('Deleted task cannot be completed')
+ 
+         self.backend.complete_task(self)
+ 
+         # Refresh the status again, so that we have updated info stored
+         self.refresh(only_fields=['status', 'start', 'end'])
+ 
+     def save(self):
+         if self.saved and not self.modified:
+             return
+ 
+         # All the actual work is done by the backend
+         self.backend.save_task(self)
+ 
+     def add_annotation(self, annotation):
+         if not self.saved:
+             raise Task.NotSaved('Task needs to be saved to add annotation')
+ 
+         self.backend.annotate_task(self, annotation)
+         self.refresh(only_fields=['annotations'])
+ 
+     def remove_annotation(self, annotation):
+         if not self.saved:
+             raise Task.NotSaved('Task needs to be saved to remove annotation')
+ 
+         if isinstance(annotation, TaskAnnotation):
+             annotation = annotation['description']
+ 
+         self.backend.denotate_task(self, annotation)
+         self.refresh(only_fields=['annotations'])
+ 
+     def refresh(self, only_fields=None, after_save=False):
+         # Raise error when trying to refresh a task that has not been saved
+         if not self.saved:
+             raise Task.NotSaved('Task needs to be saved to be refreshed')
+ 
+         new_data = self.backend.refresh_task(self, after_save=after_save)
+ 
+         if only_fields:
+             to_update = dict(
+                 [(k, new_data.get(k)) for k in only_fields],
+             )
+             self._update_data(to_update, update_original=True)
+         else:
+             self._load_data(new_data)
+ 
+ 
+ class TaskQuerySet(object):
+     """
+     Represents a lazy lookup for a task objects.
+     """
+ 
+     def __init__(self, backend, filter_obj=None):
+         self.backend = backend
+         self._result_cache = None
+         self.filter_obj = filter_obj or self.backend.filter_class(backend)
+ 
+     def __deepcopy__(self, memo):
+         """
+         Deep copy of a QuerySet doesn't populate the cache
+         """
+         obj = self.__class__(backend=self.backend)
+         for k, v in self.__dict__.items():
+             if k in ('_iter', '_result_cache'):
+                 obj.__dict__[k] = None
+             else:
+                 obj.__dict__[k] = copy.deepcopy(v, memo)
+         return obj
+ 
+     def __repr__(self):
+         data = list(self[:REPR_OUTPUT_SIZE + 1])
+         if len(data) > REPR_OUTPUT_SIZE:
+             data[-1] = '...(remaining elements truncated)...'
+         return repr(data)
+ 
+     def __len__(self):
+         if self._result_cache is None:
+             self._result_cache = list(self)
+         return len(self._result_cache)
+ 
+     def __iter__(self):
+         if self._result_cache is None:
+             self._result_cache = self._execute()
+         return iter(self._result_cache)
+ 
+     def __getitem__(self, k):
+         if self._result_cache is None:
+             self._result_cache = list(self)
+         return self._result_cache.__getitem__(k)
+ 
+     def __bool__(self):
+         if self._result_cache is not None:
+             return bool(self._result_cache)
+         try:
+             next(iter(self))
+         except StopIteration:
+             return False
+         return True
+ 
+     def __nonzero__(self):
+         return type(self).__bool__(self)
+ 
+     def _clone(self, klass=None, **kwargs):
+         if klass is None:
+             klass = self.__class__
+         filter_obj = self.filter_obj.clone()
+         c = klass(backend=self.backend, filter_obj=filter_obj)
+         c.__dict__.update(kwargs)
+         return c
+ 
+     def _execute(self):
+         """
+         Fetch the tasks which match the current filters.
+         """
+         return self.backend.filter_tasks(self.filter_obj)
+ 
+     def all(self):
+         """
+         Returns a new TaskQuerySet that is a copy of the current one.
+         """
+         return self._clone()
+ 
+     def pending(self):
+         return self.filter(status=PENDING)
+ 
+     def completed(self):
+         return self.filter(status=COMPLETED)
+ 
+     def deleted(self):
+         return self.filter(status=DELETED)
+ 
+     def waiting(self):
+         return self.filter(status=WAITING)
+ 
+     def recurring(self):
+         return self.filter(status=RECURRING)
+ 
+     def filter(self, *args, **kwargs):
+         """
+         Returns a new TaskQuerySet with the given filters added.
+         """
+         clone = self._clone()
+         for f in args:
+             clone.filter_obj.add_filter(f)
+         for key, value in kwargs.items():
+             clone.filter_obj.add_filter_param(key, value)
+         return clone
+ 
+     def get(self, **kwargs):
+         """
+         Performs the query and returns a single object matching the given
+         keyword arguments.
+         """
+         clone = self.filter(**kwargs)
+         num = len(clone)
+         if num == 1:
+             return clone._result_cache[0]
+         if not num:
+             raise Task.DoesNotExist(
+                 'Task matching query does not exist. '
+                 'Lookup parameters were {0}'.format(kwargs),
+             )
+         raise ValueError(
+             'get() returned more than one Task -- it returned {0}! '
+             'Lookup parameters were {1}'.format(num, kwargs),
+         )
 
--- /dev/null
+ # coding=utf-8
+ 
+ import copy
+ import datetime
+ import itertools
+ import json
+ import os
+ import pytz
+ import six
+ import shutil
+ import sys
+ import tempfile
+ import unittest
+ 
+ from .backends import TaskWarrior
+ from .task import Task, ReadOnlyDictView
+ from .lazy import LazyUUIDTask, LazyUUIDTaskSet
+ from .serializing import DATE_FORMAT, local_zone
+ 
+ # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
+ TASK_STANDARD_ATTRS = (
+     'status',
+     'uuid',
+     'entry',
+     'description',
+     'start',
+     'end',
+     'due',
+     'until',
+     'wait',
+     'modified',
+     'scheduled',
+     'recur',
+     'mask',
+     'imask',
+     'parent',
+     'project',
+     'priority',
+     'depends',
+     'tags',
+     'annotations',
+ )
+ 
+ 
+ def total_seconds_2_6(x):
+     return x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
+ 
+ 
+ class TasklibTest(unittest.TestCase):
+ 
+     def get_taskwarrior(self, **kwargs):
+         tw_kwargs = dict(
+             data_location=self.tmp,
+             taskrc_location='/',
+         )
+         tw_kwargs.update(kwargs)
+         return TaskWarrior(**tw_kwargs)
+ 
+     def setUp(self):
+         self.tmp = tempfile.mkdtemp(dir='.')
+         self.tw = self.get_taskwarrior()
+ 
+     def tearDown(self):
+         shutil.rmtree(self.tmp)
+ 
+ 
+ class TaskWarriorTest(TasklibTest):
+ 
+     def test_custom_command(self):
+         # ensure that a custom command which contains multiple parts
+         # is properly split up
+         tw = self.get_taskwarrior(
+             task_command='wsl task',
+             # prevent `_get_version` from running as `wsl` may not exist
+             version_override=os.getenv('TASK_VERSION'),
+         )
+         self.assertEqual(tw._get_task_command(), ['wsl', 'task'])
+ 
+ 
+ class TaskFilterTest(TasklibTest):
+ 
+     def test_all_empty(self):
+         self.assertEqual(len(self.tw.tasks.all()), 0)
+ 
+     def test_all_non_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.all()), 1)
+         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
+         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
+ 
+     def test_pending_non_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.pending()), 1)
+         self.assertEqual(
+             self.tw.tasks.pending()[0]['description'],
+             'test task',
+         )
+         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
+ 
+     def test_completed_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.completed()), 0)
+ 
+     def test_completed_non_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.completed()), 0)
+         self.tw.tasks.all()[0].done()
+         self.assertEqual(len(self.tw.tasks.completed()), 1)
+ 
+     def test_deleted_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.deleted()), 0)
+ 
+     def test_deleted_non_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.deleted()), 0)
+         self.tw.tasks.all()[0].delete()
+         self.assertEqual(len(self.tw.tasks.deleted()), 1)
+ 
+     def test_waiting_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.waiting()), 0)
+ 
+     def test_waiting_non_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.waiting()), 0)
+ 
+         t = self.tw.tasks.all()[0]
+         t['wait'] = datetime.datetime.now() + datetime.timedelta(days=1)
+         t.save()
+ 
+         self.assertEqual(len(self.tw.tasks.waiting()), 1)
+ 
+     def test_recurring_empty(self):
+         Task(self.tw, description='test task').save()
+         self.assertEqual(len(self.tw.tasks.recurring()), 0)
+ 
+     def test_recurring_non_empty(self):
+         Task(
+             self.tw,
+             description='test task',
+             recur='daily',
+             due=datetime.datetime.now(),
+         ).save()
+         self.assertEqual(len(self.tw.tasks.recurring()), 1)
+ 
+     def test_filtering_by_attribute(self):
+         Task(self.tw, description='no priority task').save()
+         Task(self.tw, priority='H', description='high priority task').save()
+         self.assertEqual(len(self.tw.tasks.all()), 2)
+ 
+         # Assert that the correct number of tasks is returned
+         self.assertEqual(len(self.tw.tasks.filter(priority='H')), 1)
+ 
+         # Assert that the correct tasks are returned
+         high_priority_task = self.tw.tasks.get(priority='H')
+         self.assertEqual(
+             high_priority_task['description'],
+             'high priority task',
+         )
+ 
+     def test_filtering_by_empty_attribute(self):
+         Task(self.tw, description='no priority task').save()
+         Task(self.tw, priority='H', description='high priority task').save()
+         self.assertEqual(len(self.tw.tasks.all()), 2)
+ 
+         # Assert that the correct number of tasks is returned
+         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
+ 
+         # Assert that the correct tasks are returned
+         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')
+ 
+     def test_filter_with_empty_uuid(self):
+         self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
+ 
+     def test_filter_dummy_by_status(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(status=t['status'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_uuid(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(uuid=t['uuid'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_entry(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(entry=t['entry'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_description(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(description=t['description'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_start(self):
+         t = Task(self.tw, description='test')
+         t.save()
+         t.start()
+ 
+         tasks = self.tw.tasks.filter(start=t['start'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_end(self):
+         t = Task(self.tw, description='test')
+         t.save()
+         t.done()
+ 
+         tasks = self.tw.tasks.filter(end=t['end'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_due(self):
+         t = Task(self.tw, description='test', due=datetime.datetime.now())
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(due=t['due'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_until(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(until=t['until'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_modified(self):
+         # Older TW version does not support bumping modified
+         # on save
+         if self.tw.version < six.text_type('2.2.0'):
+             # Python2.6 does not support SkipTest. As a workaround
+             # mark the test as passed by exiting.
+             if getattr(unittest, 'SkipTest', None) is not None:
+                 raise unittest.SkipTest()
+             else:
+                 return
+ 
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(modified=t['modified'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_scheduled(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_tags(self):
+         t = Task(self.tw, description='test', tags=['home'])
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(tags=t['tags'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_dummy_by_projects(self):
+         t = Task(self.tw, description='test', project='random')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(project=t['project'])
+         self.assertEqual(list(tasks), [t])
+ 
+     def test_filter_by_priority(self):
+         t = Task(self.tw, description='test', priority='H')
+         t.save()
+ 
+         tasks = self.tw.tasks.filter(priority=t['priority'])
+         self.assertEqual(list(tasks), [t])
+ 
+ 
+ class TaskTest(TasklibTest):
+ 
+     def test_create_unsaved_task(self):
+         # Make sure a new task is not saved unless explicitly called for
+         Task(self.tw, description='test task')
+         self.assertEqual(len(self.tw.tasks.all()), 0)
+ 
+     # TODO: once python 2.6 compatibility is over, use context managers here
+     #       and in all subsequent tests for assertRaises
+ 
+     def test_delete_unsaved_task(self):
+         t = Task(self.tw, description='test task')
+         self.assertRaises(Task.NotSaved, t.delete)
+ 
+     def test_complete_unsaved_task(self):
+         t = Task(self.tw, description='test task')
+         self.assertRaises(Task.NotSaved, t.done)
+ 
+     def test_refresh_unsaved_task(self):
+         t = Task(self.tw, description='test task')
+         self.assertRaises(Task.NotSaved, t.refresh)
+ 
+     def test_start_unsaved_task(self):
+         t = Task(self.tw, description='test task')
+         self.assertRaises(Task.NotSaved, t.start)
+ 
+     def test_delete_deleted_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.delete()
+ 
+         self.assertRaises(Task.DeletedTask, t.delete)
+ 
+     def test_complete_completed_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.done()
+ 
+         self.assertRaises(Task.CompletedTask, t.done)
+ 
+     def test_start_completed_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.done()
+ 
+         self.assertRaises(Task.CompletedTask, t.start)
+ 
+     def test_add_completed_task(self):
+         t = Task(
+             self.tw,
+             description='test',
+             status='completed',
+             end=datetime.datetime.now(),
+         )
+         t.save()
+ 
+     def test_add_multiple_completed_tasks(self):
+         t1 = Task(
+             self.tw,
+             description='test1',
+             status='completed',
+             end=datetime.datetime.now(),
+         )
+         t2 = Task(
+             self.tw,
+             description='test2',
+             status='completed',
+             end=datetime.datetime.now(),
+         )
+         t1.save()
+         t2.save()
+ 
+     def test_complete_deleted_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.delete()
+ 
+         self.assertRaises(Task.DeletedTask, t.done)
+ 
+     def test_starting_task(self):
+         t = Task(self.tw, description='test task')
+         now = t.datetime_normalizer(datetime.datetime.now())
+         t.save()
+         t.start()
+ 
+         self.assertTrue(now.replace(microsecond=0) <= t['start'])
+         self.assertEqual(t['status'], 'pending')
+ 
+     def test_completing_task(self):
+         t = Task(self.tw, description='test task')
+         now = t.datetime_normalizer(datetime.datetime.now())
+         t.save()
+         t.done()
+ 
+         self.assertTrue(now.replace(microsecond=0) <= t['end'])
+         self.assertEqual(t['status'], 'completed')
+ 
+     def test_deleting_task(self):
+         t = Task(self.tw, description='test task')
+         now = t.datetime_normalizer(datetime.datetime.now())
+         t.save()
+         t.delete()
+ 
+         self.assertTrue(now.replace(microsecond=0) <= t['end'])
+         self.assertEqual(t['status'], 'deleted')
+ 
+     def test_started_task_active(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.start()
+         self.assertTrue(t.active)
+ 
+     def test_unstarted_task_inactive(self):
+         t = Task(self.tw, description='test task')
+         self.assertFalse(t.active)
+         t.save()
+         self.assertFalse(t.active)
+ 
+     def test_start_active_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.start()
+         self.assertRaises(Task.ActiveTask, t.start)
+ 
+     def test_stop_completed_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.start()
+         t.done()
+ 
+         self.assertRaises(Task.InactiveTask, t.stop)
+ 
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.done()
+ 
+         self.assertRaises(Task.InactiveTask, t.stop)
+ 
+     def test_stop_deleted_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.start()
+         t.delete()
+         t.stop()
+ 
+     def test_stop_inactive_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+ 
+         self.assertRaises(Task.InactiveTask, t.stop)
+ 
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.start()
+         t.stop()
+ 
+         self.assertRaises(Task.InactiveTask, t.stop)
+ 
+     def test_stopping_task(self):
+         t = Task(self.tw, description='test task')
+         t.datetime_normalizer(datetime.datetime.now())
+         t.save()
+         t.start()
+         t.stop()
+ 
+         self.assertEqual(t['end'], None)
+         self.assertEqual(t['status'], 'pending')
+         self.assertFalse(t.active)
+ 
+     def test_modify_simple_attribute_without_space(self):
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         self.assertEqual(t['description'], 'test')
+ 
+         t['description'] = 'test-modified'
+         t.save()
+ 
+         self.assertEqual(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.assertEqual(t['description'], 'test task')
+ 
+         t['description'] = 'test task modified'
+         t.save()
+ 
+         self.assertEqual(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()
+         t['depends'] = set([dependency])
+ 
+         self.assertRaises(Task.NotSaved, t.save)
+ 
+     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_simple_dependency_lazyuuidtaskset(self):
+         # Adds only one dependency as a LazyUUIDTaskSet 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'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
+ 
+     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_set_complex_dependency_lazyuuidtaskset(self):
+         # Adds two dependencies as a LazyUUIDTaskSet 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'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']])
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
+ 
+     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'].remove(dependency2)
+         t.save()
+ 
+         self.assertEqual(t['depends'], set([dependency1]))
+ 
+     def test_remove_from_dependency_lazyuuidtaskset(self):
+         # Removes dependency from task with two dependencies as LazyUUIDTaskSet
+         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'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']])
+         t.save()
+ 
+         t['depends'].remove(dependency2)
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid']]))
+ 
+     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'].add(dependency2)
+         t.save()
+ 
+         self.assertEqual(t['depends'], set([dependency1, dependency2]))
+ 
+     def test_add_to_dependency_lazyuuidtaskset(self):
+         # Adds dependency to task with one dependencies as LazyUUIDTaskSet
+         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'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid']])
+         t.save()
+ 
+         t['depends'].add(dependency2)
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
+ 
+     def test_add_lazyuuidtaskset_to_dependency_lazyuuidtaskset(self):
+         # Adds dependency as LazyUUIDTaskSet to task with one dependencies as LazyUUIDTaskSet
+         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'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid']])
+         t.save()
+ 
+         t['depends'] = LazyUUIDTaskSet(self.tw, [dependency2['uuid']]).union(t['depends'])
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
+ 
+     def test_add_to_empty_dependency_set(self):
+         # Adds 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'].add(dependency)
+         t.save()
+ 
+         self.assertEqual(t['depends'], set([dependency]))
+ 
+     def test_add_to_empty_dependency_lazyuuidtaskset(self):
+         # Adds dependency as LazyUUIDTaskSet 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'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
+ 
+     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_simple_dependency_lazyuuidtaskset_save_repeatedly(self):
+         # Adds only one dependency as LazyUUIDTaskSet 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'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
+         t.save()
+ 
+         # We taint the task, but keep depends intact
+         t['description'] = 'test task modified'
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
+ 
+         # We taint the task, but assign the same set to the depends
+         t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
+         t['description'] = 'test task modified again'
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
+ 
+     def test_simple_dependency_lazyuuidtaskset_save_before_repeatedly(self):
+         # Adds only one dependency as LazyUUIDTaskSet to a saved task with no dependencies
+         t = Task(self.tw, description='test task')
+         dependency = Task(self.tw, description='needs to be done first')
+         dependency.save()
+         t.save()
+ 
+         t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
+         t.save()
+ 
+         self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
+ 
+     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_hash_unequal_unsaved_tasks(self):
+         # Compare the hash of the task using two different objects
+         t1 = Task(self.tw, description='test task 1')
+         t2 = Task(self.tw, description='test task 2')
+ 
+         self.assertNotEqual(t1.__hash__(), t2.__hash__())
+ 
+     def test_hash_unequal_saved_tasks(self):
+         # Compare the hash of the task using two different objects
+         t1 = Task(self.tw, description='test task 1')
+         t2 = Task(self.tw, description='test task 2')
+ 
+         t1.save()
+         t2.save()
+ 
+         self.assertNotEqual(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
+         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_modified_fields_not_affected_by_reading(self):
+         t = Task(self.tw)
+ 
+         for field in TASK_STANDARD_ATTRS:
+             t[field]
+ 
+         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'),
+             )
+ 
+     def test_saving_unmodified_task(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t.save()
+ 
+     def test_adding_tag_by_appending(self):
+         t = Task(self.tw, description='test task', tags=['test1'])
+         t.save()
+         t['tags'].add('test2')
+         t.save()
+         self.assertEqual(t['tags'], set(['test1', 'test2']))
+ 
+     def test_adding_tag_twice(self):
+         t = Task(self.tw, description='test task', tags=['test1'])
+         t.save()
+         t['tags'].add('test2')
+         t['tags'].add('test2')
+         t.save()
+         self.assertEqual(t['tags'], set(['test1', 'test2']))
+ 
+     def test_adding_tag_by_appending_empty(self):
+         t = Task(self.tw, description='test task')
+         t.save()
+         t['tags'].add('test')
+         t.save()
+         self.assertEqual(t['tags'], set(['test']))
+ 
+     def test_serializers_returning_empty_string_for_none(self):
+         # Test that any serializer returns '' when passed None
+         t = Task(self.tw)
+         serializers = [
+             getattr(t, serializer_name)
+             for serializer_name in filter(
+                 lambda x: x.startswith('serialize_'),
+                 dir(t),
+             )
+         ]
+         for serializer in serializers:
+             self.assertEqual(serializer(None), '')
+ 
+     def test_deserializer_returning_empty_value_for_empty_string(self):
+         # Test that any deserializer returns empty value when passed ''
+         t = Task(self.tw)
+         deserializers = [
+             getattr(t, deserializer_name)
+             for deserializer_name in filter(
+                 lambda x: x.startswith('deserialize_'),
+                 dir(t),
+             )
+         ]
+         for deserializer in deserializers:
+             self.assertTrue(deserializer('') in (None, [], set()))
+ 
+     def test_normalizers_handling_none(self):
+         # Test that any normalizer can handle None as a valid value
+         t = Task(self.tw)
+ 
+         for key in TASK_STANDARD_ATTRS:
+             t._normalize(key, None)
+ 
+     def test_recurrent_task_generation(self):
+         today = datetime.date.today()
+         t = Task(
+             self.tw,
+             description='brush teeth',
+             due=today,
+             recur='daily',
+         )
+         t.save()
+         self.assertEqual(len(self.tw.tasks.pending()), 2)
+ 
+     def test_spawned_task_parent(self):
+         today = datetime.date.today()
+         t = Task(
+             self.tw,
+             description='brush teeth',
+             due=today,
+             recur='daily',
+         )
+         t.save()
+ 
+         spawned = self.tw.tasks.pending().get(due=today)
+         assert spawned['parent'] == t
+ 
+     def test_modify_number_of_tasks_at_once(self):
+         for i in range(1, 100):
+             Task(self.tw, description='test task %d' % i, tags=['test']).save()
+ 
+         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
+ 
+     def test_return_all_from_executed_command(self):
+         Task(self.tw, description='test task', tags=['test']).save()
+         out, err, rc = self.tw.execute_command(['count'], return_all=True)
+         self.assertEqual(rc, 0)
+ 
+     def test_return_all_from_failed_executed_command(self):
+         Task(self.tw, description='test task', tags=['test']).save()
+         out, err, rc = self.tw.execute_command(
+             ['countinvalid'],
+             return_all=True,
+             allow_failure=False,
+         )
+         self.assertNotEqual(rc, 0)
+ 
+ 
+ class TaskFromHookTest(TasklibTest):
+ 
+     input_add_data = six.StringIO(
+         '{"description":"Buy some milk",'
+         '"entry":"20141118T050231Z",'
+         '"status":"pending",'
+         '"start":"20141119T152233Z",'
+         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}',
+     )
+ 
+     input_add_data_recurring = six.StringIO(
+         '{"description":"Mow the lawn",'
+         '"entry":"20160210T224304Z",'
+         '"parent":"62da6227-519c-42c2-915d-dccada926ad7",'
+         '"recur":"weekly",'
+         '"status":"pending",'
+         '"uuid":"81305335-0237-49ff-8e87-b3cdc2369cec"}',
+     )
+ 
+     input_modify_data = six.StringIO(
+         '\n'.join([
+             input_add_data.getvalue(),
+             (
+                 '{"description":"Buy some milk finally",'
+                 '"entry":"20141118T050231Z",'
+                 '"status":"completed",'
+                 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}'
+             ),
+         ]),
+     )
+ 
+     exported_raw_data = (
+         '{"project":"Home",'
+         '"due":"20150101T232323Z",'
+         '"description":"test task"}'
+     )
+ 
+     def test_setting_up_from_add_hook_input(self):
+         t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
+         self.assertEqual(t['description'], 'Buy some milk')
+         self.assertEqual(t.pending, True)
+ 
+     def test_setting_up_from_add_hook_input_recurring(self):
+         t = Task.from_input(
+             input_file=self.input_add_data_recurring,
+             backend=self.tw,
+         )
+         self.assertEqual(t['description'], 'Mow the lawn')
+         self.assertEqual(t.pending, True)
+ 
+     def test_setting_up_from_modified_hook_input(self):
+         t = Task.from_input(
+             input_file=self.input_modify_data,
+             modify=True,
+             backend=self.tw,
+         )
+         self.assertEqual(t['description'], 'Buy some milk finally')
+         self.assertEqual(t.pending, False)
+         self.assertEqual(t.completed, True)
+ 
+         self.assertEqual(t._original_data['status'], 'pending')
+         self.assertEqual(t._original_data['description'], 'Buy some milk')
+         self.assertEqual(
+             set(t._modified_fields),
+             set(['status', 'description', 'start']),
+         )
+ 
+     def test_export_data(self):
+         t = Task(
+             self.tw,
+             description='test task',
+             project='Home',
+             due=pytz.utc.localize(
+                 datetime.datetime(2015, 1, 1, 23, 23, 23)),
+         )
+ 
+         # Check that the output is a permutation of:
+         # {"project":"Home","description":"test task","due":"20150101232323Z"}
+         allowed_segments = self.exported_raw_data[1:-1].split(',')
+         allowed_output = [
+             '{' + ','.join(segments) + '}'
+             for segments in itertools.permutations(allowed_segments)
+         ]
+ 
+         self.assertTrue(
+             any(t.export_data() == expected
+                 for expected in allowed_output),
+         )
+ 
+ 
+ class TimezoneAwareDatetimeTest(TasklibTest):
+ 
+     def setUp(self):
+         super(TimezoneAwareDatetimeTest, self).setUp()
+         self.zone = local_zone
+         self.localdate_naive = datetime.datetime(2015, 2, 2)
+         self.localtime_naive = datetime.datetime(2015, 2, 2, 0, 0, 0)
+         self.localtime_aware = self.zone.localize(self.localtime_naive)
+         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
+ 
+     def test_timezone_naive_datetime_setitem(self):
+         t = Task(self.tw, description='test task')
+         t['due'] = self.localtime_naive
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_timezone_naive_datetime_using_init(self):
+         t = Task(self.tw, description='test task', due=self.localtime_naive)
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_filter_by_naive_datetime(self):
+         t = Task(self.tw, description='task1', due=self.localtime_naive)
+         t.save()
+         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
+         self.assertEqual(len(matching_tasks), 1)
+ 
+     def test_serialize_naive_datetime(self):
+         t = Task(self.tw, description='task1', due=self.localtime_naive)
+         self.assertEqual(
+             json.loads(t.export_data())['due'],
+             self.utctime_aware.strftime(DATE_FORMAT),
+         )
+ 
+     def test_timezone_naive_date_setitem(self):
+         t = Task(self.tw, description='test task')
+         t['due'] = self.localdate_naive
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_timezone_naive_date_using_init(self):
+         t = Task(self.tw, description='test task', due=self.localdate_naive)
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_filter_by_naive_date(self):
+         t = Task(self.tw, description='task1', due=self.localdate_naive)
+         t.save()
+         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
+         self.assertEqual(len(matching_tasks), 1)
+ 
+     def test_serialize_naive_date(self):
+         t = Task(self.tw, description='task1', due=self.localdate_naive)
+         self.assertEqual(
+             json.loads(t.export_data())['due'],
+             self.utctime_aware.strftime(DATE_FORMAT),
+         )
+ 
+     def test_timezone_aware_datetime_setitem(self):
+         t = Task(self.tw, description='test task')
+         t['due'] = self.localtime_aware
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_timezone_aware_datetime_using_init(self):
+         t = Task(self.tw, description='test task', due=self.localtime_aware)
+         self.assertEqual(t['due'], self.localtime_aware)
+ 
+     def test_filter_by_aware_datetime(self):
+         t = Task(self.tw, description='task1', due=self.localtime_aware)
+         t.save()
+         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
+         self.assertEqual(len(matching_tasks), 1)
+ 
+     def test_serialize_aware_datetime(self):
+         t = Task(self.tw, description='task1', due=self.localtime_aware)
+         self.assertEqual(
+             json.loads(t.export_data())['due'],
+             self.utctime_aware.strftime(DATE_FORMAT),
+         )
+ 
+ 
+ class DatetimeStringTest(TasklibTest):
+ 
+     def test_simple_now_conversion(self):
+         if self.tw.version < six.text_type('2.4.0'):
+             # Python2.6 does not support SkipTest. As a workaround
+             # mark the test as passed by exiting.
+             if getattr(unittest, 'SkipTest', None) is not None:
+                 raise unittest.SkipTest()
+             else:
+                 return
+ 
+         t = Task(self.tw, description='test task', due='now')
+         now = local_zone.localize(datetime.datetime.now())
+ 
+         # Assert that both times are not more than 5 seconds apart
+         if sys.version_info < (2, 7):
+             self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
+             self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
+         else:
+             self.assertTrue((now - t['due']).total_seconds() < 5)
+             self.assertTrue((t['due'] - now).total_seconds() < 5)
+ 
+     def test_simple_eoy_conversion(self):
+         if self.tw.version < six.text_type('2.4.0'):
+             # Python2.6 does not support SkipTest. As a workaround
+             # mark the test as passed by exiting.
+             if getattr(unittest, 'SkipTest', None) is not None:
+                 raise unittest.SkipTest()
+             else:
+                 return
+ 
+         t = Task(self.tw, description='test task', due='eoy')
+         now = local_zone.localize(datetime.datetime.now())
+         eoy = local_zone.localize(datetime.datetime(
+             year=now.year,
+             month=12,
+             day=31,
+             hour=23,
+             minute=59,
+             second=59,
+             ))
+         self.assertEqual(eoy, t['due'])
+ 
+     def test_complex_eoy_conversion(self):
+         if self.tw.version < six.text_type('2.4.0'):
+             # Python2.6 does not support SkipTest. As a workaround
+             # mark the test as passed by exiting.
+             if getattr(unittest, 'SkipTest', None) is not None:
+                 raise unittest.SkipTest()
+             else:
+                 return
+ 
+         t = Task(self.tw, description='test task', due='eoy - 4 months')
+         now = local_zone.localize(datetime.datetime.now())
+         due_date = local_zone.localize(
+             datetime.datetime(
+                 year=now.year,
+                 month=12,
+                 day=31,
+                 hour=23,
+                 minute=59,
+                 second=59,
+             )
+         ) - datetime.timedelta(0, 4 * 30 * 86400)
+         self.assertEqual(due_date, t['due'])
+ 
+     def test_filtering_with_string_datetime(self):
+         if self.tw.version < six.text_type('2.4.0'):
+             # Python2.6 does not support SkipTest. As a workaround
+             # mark the test as passed by exiting.
+             if getattr(unittest, 'SkipTest', None) is not None:
+                 raise unittest.SkipTest()
+             else:
+                 return
+ 
+         t = Task(
+             self.tw,
+             description='test task',
+             due=datetime.datetime.now() - datetime.timedelta(0, 2),
+         )
+         t.save()
+         self.assertEqual(len(self.tw.tasks.filter(due__before='now')), 1)
+ 
+ 
+ class AnnotationTest(TasklibTest):
+ 
+     def setUp(self):
+         super(AnnotationTest, self).setUp()
+         Task(self.tw, description='test task').save()
+ 
+     def test_adding_annotation(self):
+         task = self.tw.tasks.get()
+         task.add_annotation('test annotation')
+         self.assertEqual(len(task['annotations']), 1)
+         ann = task['annotations'][0]
+         self.assertEqual(ann['description'], 'test annotation')
+ 
+     def test_removing_annotation(self):
+         task = self.tw.tasks.get()
+         task.add_annotation('test annotation')
+         ann = task['annotations'][0]
+         ann.remove()
+         self.assertEqual(len(task['annotations']), 0)
+ 
+     def test_removing_annotation_by_description(self):
+         task = self.tw.tasks.get()
+         task.add_annotation('test annotation')
+         task.remove_annotation('test annotation')
+         self.assertEqual(len(task['annotations']), 0)
+ 
+     def test_removing_annotation_by_obj(self):
+         task = self.tw.tasks.get()
+         task.add_annotation('test annotation')
+         ann = task['annotations'][0]
+         task.remove_annotation(ann)
+         self.assertEqual(len(task['annotations']), 0)
+ 
+     def test_annotation_after_modification(self):
+         task = self.tw.tasks.get()
+         task['project'] = 'test'
+         task.add_annotation('I should really do this task')
+         self.assertEqual(task['project'], 'test')
+         task.save()
+         self.assertEqual(task['project'], 'test')
+ 
+     def test_serialize_annotations(self):
+         # Test that serializing annotations is possible
+         t = Task(self.tw, description='test')
+         t.save()
+ 
+         t.add_annotation('annotation1')
+         t.add_annotation('annotation2')
+ 
+         data = t._serialize('annotations', t._data['annotations'])
+ 
+         self.assertEqual(len(data), 2)
+         self.assertEqual(type(data[0]), dict)
+         self.assertEqual(type(data[1]), dict)
+ 
+         self.assertEqual(data[0]['description'], 'annotation1')
+         self.assertEqual(data[1]['description'], 'annotation2')
+ 
+ 
+ class UnicodeTest(TasklibTest):
+ 
+     def test_unicode_task(self):
+         Task(self.tw, description=six.u('†åßk')).save()
+         self.tw.tasks.get()
+ 
+     def test_filter_by_unicode_task(self):
+         Task(self.tw, description=six.u('†åßk')).save()
+         tasks = self.tw.tasks.filter(description=six.u('†åßk'))
+         self.assertEqual(len(tasks), 1)
+ 
+     def test_non_unicode_task(self):
+         Task(self.tw, description='test task').save()
+         self.tw.tasks.get()
+ 
+ 
+ class ReadOnlyDictViewTest(unittest.TestCase):
+ 
+     def setUp(self):
+         self.sample = dict(sample_list=[1, 2, 3], sample_dict={'key': 'value'})
+         self.original_sample = copy.deepcopy(self.sample)
+         self.view = ReadOnlyDictView(self.sample)
+ 
+     def test_readonlydictview_getitem(self):
+         sample_list = self.view['sample_list']
+         self.assertEqual(sample_list, self.sample['sample_list'])
+ 
+         # Assert that modification changed only copied value
+         sample_list.append(4)
+         self.assertNotEqual(sample_list, self.sample['sample_list'])
+ 
+         # Assert that viewed dict is not changed
+         self.assertEqual(self.sample, self.original_sample)
+ 
+     def test_readonlydictview_contains(self):
+         self.assertEqual('sample_list' in self.view,
+                          'sample_list' in self.sample)
+         self.assertEqual('sample_dict' in self.view,
+                          'sample_dict' in self.sample)
+         self.assertEqual('key' in self.view, 'key' in self.sample)
+ 
+         # Assert that viewed dict is not changed
+         self.assertEqual(self.sample, self.original_sample)
+ 
+     def test_readonlydictview_iter(self):
+         self.assertEqual(
+             list(key for key in self.view),
+             list(key for key in self.sample),
+         )
+ 
+         # Assert the view is correct after modification
+         self.sample['new'] = 'value'
+         self.assertEqual(
+             list(key for key in self.view),
+             list(key for key in self.sample),
+         )
+ 
+     def test_readonlydictview_len(self):
+         self.assertEqual(len(self.view), len(self.sample))
+ 
+         # Assert the view is correct after modification
+         self.sample['new'] = 'value'
+         self.assertEqual(len(self.view), len(self.sample))
+ 
+     def test_readonlydictview_get(self):
+         sample_list = self.view.get('sample_list')
+         self.assertEqual(sample_list, self.sample.get('sample_list'))
+ 
+         # Assert that modification changed only copied value
+         sample_list.append(4)
+         self.assertNotEqual(sample_list, self.sample.get('sample_list'))
+ 
+         # Assert that viewed dict is not changed
+         self.assertEqual(self.sample, self.original_sample)
+ 
+     def test_readonlydict_items(self):
+         view_items = self.view.items()
+         sample_items = list(self.sample.items())
+         self.assertEqual(view_items, sample_items)
+ 
+         view_items.append('newkey')
+         self.assertNotEqual(view_items, sample_items)
+         self.assertEqual(self.sample, self.original_sample)
+ 
+     def test_readonlydict_values(self):
+         view_values = self.view.values()
+         sample_values = list(self.sample.values())
+         self.assertEqual(view_values, sample_values)
+ 
+         view_list_item = list(filter(lambda x: type(x) is list,
+                                      view_values))[0]
+         view_list_item.append(4)
+         self.assertNotEqual(view_values, sample_values)
+         self.assertEqual(self.sample, self.original_sample)
+ 
+ 
+ class LazyUUIDTaskTest(TasklibTest):
+ 
+     def setUp(self):
+         super(LazyUUIDTaskTest, self).setUp()
+ 
+         self.stored = Task(self.tw, description='this is test task')
+         self.stored.save()
+ 
+         self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
+ 
+     def test_uuid_non_conversion(self):
+         assert self.stored['uuid'] == self.lazy['uuid']
+         assert type(self.lazy) is LazyUUIDTask
+ 
+     def test_lazy_explicit_conversion(self):
+         assert type(self.lazy) is LazyUUIDTask
+         self.lazy.replace()
+         assert type(self.lazy) is Task
+ 
+     def test_conversion_key(self):
+         assert self.stored['description'] == self.lazy['description']
+         assert type(self.lazy) is Task
+ 
+     def test_conversion_attribute(self):
+         assert type(self.lazy) is LazyUUIDTask
+         assert self.lazy.completed is False
+         assert type(self.lazy) is Task
+ 
+     def test_normal_to_lazy_equality(self):
+         assert self.stored == self.lazy
+         assert not self.stored != self.lazy
+         assert type(self.lazy) is LazyUUIDTask
+ 
+     def test_lazy_to_lazy_equality(self):
+         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
+         lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
+ 
+         assert lazy1 == lazy2
+         assert not lazy1 != lazy2
+         assert type(lazy1) is LazyUUIDTask
+         assert type(lazy2) is LazyUUIDTask
+ 
+     def test_normal_to_lazy_inequality(self):
+         # Create a different UUID by changing the last letter
+         wrong_uuid = self.stored['uuid']
+         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
+ 
+         wrong_lazy = LazyUUIDTask(self.tw, wrong_uuid)
+ 
+         assert not self.stored == wrong_lazy
+         assert self.stored != wrong_lazy
+         assert type(wrong_lazy) is LazyUUIDTask
+ 
+     def test_lazy_to_lazy_inequality(self):
+         # Create a different UUID by changing the last letter
+         wrong_uuid = self.stored['uuid']
+         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
+ 
+         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
+         lazy2 = LazyUUIDTask(self.tw, wrong_uuid)
+ 
+         assert not lazy1 == lazy2
+         assert lazy1 != lazy2
+         assert type(lazy1) is LazyUUIDTask
+         assert type(lazy2) is LazyUUIDTask
+ 
+     def test_lazy_in_queryset(self):
+         tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
+ 
+         assert self.lazy in tasks
+         assert type(self.lazy) is LazyUUIDTask
+ 
+     def test_lazy_saved(self):
+         assert self.lazy.saved is True
+ 
+     def test_lazy_modified(self):
+         assert self.lazy.modified is False
+ 
+     def test_lazy_modified_fields(self):
+         assert self.lazy._modified_fields == set()
+ 
+ 
+ class LazyUUIDTaskSetTest(TasklibTest):
+ 
+     def setUp(self):
+         super(LazyUUIDTaskSetTest, self).setUp()
+ 
+         self.task1 = Task(self.tw, description='task 1')
+         self.task2 = Task(self.tw, description='task 2')
+         self.task3 = Task(self.tw, description='task 3')
+ 
+         self.task1.save()
+         self.task2.save()
+         self.task3.save()
+ 
+         self.uuids = (
+             self.task1['uuid'],
+             self.task2['uuid'],
+             self.task3['uuid'],
+         )
+ 
+         self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
+ 
+     def test_length(self):
+         assert len(self.lazy) == 3
+         assert type(self.lazy) is LazyUUIDTaskSet
+ 
+     def test_contains(self):
+         assert self.task1 in self.lazy
+         assert self.task2 in self.lazy
+         assert self.task3 in self.lazy
+         assert type(self.lazy) is LazyUUIDTaskSet
+ 
+     def test_eq_lazy(self):
+         new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
+         assert self.lazy == new_lazy
+         assert not self.lazy != new_lazy
+         assert type(self.lazy) is LazyUUIDTaskSet
+ 
+     def test_eq_real(self):
+         assert self.lazy == self.tw.tasks.all()
+         assert self.tw.tasks.all() == self.lazy
+         assert not self.lazy != self.tw.tasks.all()
+ 
+         assert type(self.lazy) is LazyUUIDTaskSet
+ 
+     def test_union(self):
+         taskset = set([self.task1])
+         lazyset = LazyUUIDTaskSet(
+             self.tw,
+             (self.task2['uuid'], self.task3['uuid']),
+         )
+ 
+         assert taskset | lazyset == self.lazy
+         assert lazyset | taskset == self.lazy
+         assert taskset.union(lazyset) == self.lazy
+         assert lazyset.union(taskset) == self.lazy
+ 
+         lazyset |= taskset
+         assert lazyset == self.lazy
+ 
+     def test_difference(self):
+         taskset = set([self.task1, self.task2])
+         lazyset = LazyUUIDTaskSet(
+             self.tw,
+             (self.task2['uuid'], self.task3['uuid']),
+         )
+ 
+         assert taskset - lazyset == set([self.task1])
+         assert lazyset - taskset == set([self.task3])
+         assert taskset.difference(lazyset) == set([self.task1])
+         assert lazyset.difference(taskset) == set([self.task3])
+ 
+         lazyset -= taskset
+         assert lazyset == set([self.task3])
+ 
+     def test_symmetric_difference(self):
+         taskset = set([self.task1, self.task2])
+         lazyset = LazyUUIDTaskSet(
+             self.tw,
+             (self.task2['uuid'], self.task3['uuid']),
+         )
+ 
+         assert taskset ^ lazyset == set([self.task1, self.task3])
+         assert lazyset ^ taskset == set([self.task1, self.task3])
+         self.assertEqual(
+             taskset.symmetric_difference(lazyset),
+             set([self.task1, self.task3]),
+         )
+         self.assertEqual(
+             lazyset.symmetric_difference(taskset),
+             set([self.task1, self.task3]),
+         )
+ 
+         lazyset ^= taskset
+         assert lazyset == set([self.task1, self.task3])
+ 
+     def test_intersection(self):
+         taskset = set([self.task1, self.task2])
+         lazyset = LazyUUIDTaskSet(
+             self.tw,
+             (self.task2['uuid'], self.task3['uuid']),
+         )
+ 
+         assert taskset & lazyset == set([self.task2])
+         assert lazyset & taskset == set([self.task2])
+         assert taskset.intersection(lazyset) == set([self.task2])
+         assert lazyset.intersection(taskset) == set([self.task2])
+ 
+         lazyset &= taskset
+         assert lazyset == set([self.task2])
+ 
+ 
+ class TaskWarriorBackendTest(TasklibTest):
+ 
+     def test_config(self):
+         assert self.tw.config['nag'] == 'You have more urgent tasks.'
+         assert self.tw.config['default.command'] == 'next'
+         assert self.tw.config['dependency.indicator'] == 'D'