From e1f497291ac12848b4cefc89068803d1867d0702 Mon Sep 17 00:00:00 2001 From: Adam Coddington Date: Mon, 14 Apr 2014 23:12:34 -0700 Subject: [PATCH] Preserve all annotation information should we have it, but still handle outgoing and incoming values as if they were strings. --- taskw/fields/annotationarray.py | 69 ++++++++++++++++++++++++++-- taskw/test/test_fields.py | 79 ++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/taskw/fields/annotationarray.py b/taskw/fields/annotationarray.py index ba520c6..0a59b83 100644 --- a/taskw/fields/annotationarray.py +++ b/taskw/fields/annotationarray.py @@ -1,14 +1,63 @@ +import sys + +from dateutil.parser import parse +import six + from .array import ArrayField from .base import DirtyableList +class Annotation(object): + """ A special type of string that we'll use for storing annotations. + + This is, for all intents and purposes, really just a string, but + it does allow us to store additional information if we have it -- in + this application: the annotation's entry date. + + """ + def __init__(self, description, entry=None): + self._description = description + self._entry = entry + super(Annotation, self).__init__() + + @property + def entry(self): + if self._entry: + return parse(self._entry) + return self._entry + + def __str__(self): + if six.PY3: + return six.text_type(self._description) + return self.__unicode__().encode(sys.getdefaultencoding()) + + def __eq__(self, other): + value = other + if isinstance(other, Annotation): + value = other._description + return self._description == value + + def __ne__(self, other): + value = other + if isinstance(other, Annotation): + value = other._description + return self._description != value + + def __unicode__(self): + return six.text_type(self._description) + + def __repr__(self): + return repr(six.text_type(self._description)) + + class AnnotationArrayField(ArrayField): """ A special case of the ArrayField handling idiosyncrasies of Annotations Taskwarrior will currently return to you a dictionary of values -- the annotation's date and description -- for each annotation, but - given that we cannot create annotations with a date, let's pretend - that they aren't giving us a date. It'll simplify things a bit. + given that we cannot create annotations with a date, let's instead + return something that behaves like a string (but from which you can + extract an entry date if one exists). """ def deserialize(self, value): @@ -18,8 +67,20 @@ def deserialize(self, value): elements = [] for annotation in value: if isinstance(annotation, dict): - elements.append(annotation['description']) + elements.append( + Annotation( + annotation['description'], + annotation['entry'], + ) + ) else: - elements.append(annotation) + elements.append(Annotation(annotation)) return super(AnnotationArrayField, self).deserialize(elements) + + def serialize(self, value): + if not value: + value = [] + return super(AnnotationArrayField, self).serialize( + [six.text_type(entry) for entry in value] + ) diff --git a/taskw/test/test_fields.py b/taskw/test/test_fields.py index 6f10729..a720468 100644 --- a/taskw/test/test_fields.py +++ b/taskw/test/test_fields.py @@ -1,18 +1,93 @@ import datetime -import sys import uuid from dateutil.tz import tzlocal from pytz import UTC, timezone +import six from taskw import fields +from taskw.fields.annotationarray import Annotation -if sys.version_info >= (3, ): +if six.PY3: from unittest import TestCase else: from unittest2 import TestCase +class TestAnnotationArrayField(TestCase): + def setUp(self): + self.field = fields.AnnotationArrayField() + + def test_serialize_none(self): + actual_result = self.field.serialize(None) + expected_result = [] + + self.assertEqual(actual_result, expected_result) + + def test_serialize_annotations_into_strings(self): + value = [ + Annotation("something", "20240101T010101Z"), + Annotation("something else") + ] + + expected_serialized = ["something", "something else"] + actual_serialized = self.field.serialize(value) + + self.assertEqual(actual_serialized, expected_serialized) + for entry in actual_serialized: + self.assertTrue( + isinstance(entry, six.text_type) + ) + + def test_deserialize_fully_formed_entries_to_stringey_things(self): + # Note that this test is *identical* in conditions and actions + # to the below, but we are asserting that we can extract treat + # the returned entries just as if they were strings. + value = [ + { + 'description': 'Coddingtonbear\'s birthday', + 'entry': '19840302T000000Z' + }, + { + 'description': 'Coddingtonbear\'s partner\'s birthday', + 'entry': '19850711T000000Z', + } + ] + + expected_results = [ + value[0]['description'], + value[1]['description'], + ] + actual_results = self.field.deserialize(value) + + self.assertEqual(expected_results, actual_results) + + def test_deserialize_fully_formed_entries_to_annotation_objects(self): + # Note that this test is *identical* in conditions and actions + # to the above, but we are asserting that we can extract a little + # bit more information from the returned objects. + value = [ + { + 'description': 'Coddingtonbear\'s birthday', + 'entry': '19840302T000000Z' + }, + { + 'description': 'Coddingtonbear\'s partner\'s birthday', + 'entry': '19850711T000000Z', + } + ] + + expected_results = [ + Annotation(value[0]['description'], value[0]['entry']), + Annotation(value[1]['description'], value[1]['entry']), + ] + actual_results = self.field.deserialize(value) + + self.assertEqual(expected_results, actual_results) + self.assertEqual(expected_results[0].entry.year, 1984) + self.assertEqual(expected_results[1].entry.year, 1985) + + class TestArrayField(TestCase): def setUp(self): self.field = fields.ArrayField()