From c4d8c861713743f9bab0e95325c2af515a9182e5 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 19:45:59 +0200 Subject: [PATCH 01/16] TaskWarrior: Reorder methods --- tasklib/backends.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index a4d074e..ad5bd6b 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -144,6 +144,17 @@ class TaskWarrior(object): if self.version < 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() @@ -190,11 +201,3 @@ class TaskWarrior(object): # altering the data before saving task.refresh(after_save=True) - 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']) -- 2.39.5 From 7b0f22cf6cf519ece57e5282412669f460859293 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 19:51:11 +0200 Subject: [PATCH 02/16] Task: Move TW-specific deletion logic into TW backend --- tasklib/backends.py | 3 +++ tasklib/task.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index ad5bd6b..6bf11bb 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -201,3 +201,6 @@ class TaskWarrior(object): # altering the data before saving task.refresh(after_save=True) + def delete_task(self, task): + self.execute_command([task['uuid'], 'delete']) + diff --git a/tasklib/task.py b/tasklib/task.py index 7c6dff8..61f944a 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -586,7 +586,7 @@ class Task(TaskResource): if self.deleted: raise Task.DeletedTask("Task was already deleted") - self.warrior.execute_command([self['uuid'], 'delete']) + self.backend.delete_task(self) # Refresh the status again, so that we have updated info stored self.refresh(only_fields=['status', 'start', 'end']) -- 2.39.5 From dcbdcdd11c981b4600ea4a5522230d0456e45d21 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 19:58:18 +0200 Subject: [PATCH 03/16] Task: Move TW-specific start/stop logic into TW backend --- tasklib/backends.py | 5 +++++ tasklib/task.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index 6bf11bb..12a9234 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -204,3 +204,8 @@ class TaskWarrior(object): 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']) diff --git a/tasklib/task.py b/tasklib/task.py index 61f944a..b7bba4b 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -605,7 +605,7 @@ class Task(TaskResource): elif self.active: raise Task.ActiveTask("Task is already active") - self.warrior.execute_command([self['uuid'], 'start']) + self.backend.start_task(self) # Refresh the status again, so that we have updated info stored self.refresh(only_fields=['status', 'start']) @@ -620,7 +620,7 @@ class Task(TaskResource): if not self.active: raise Task.InactiveTask("Cannot stop an inactive task") - self.warrior.execute_command([self['uuid'], 'stop']) + self.backend.stop_task(self) # Refresh the status again, so that we have updated info stored self.refresh(only_fields=['status', 'start']) -- 2.39.5 From 5c5b35a097608b63c3d54acf2f07d5cb00e85796 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 20:00:00 +0200 Subject: [PATCH 04/16] backends: Add complete_task to the backend interface Marking task as 'done' needs to be implemented as a separate backend operation too. --- tasklib/backends.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tasklib/backends.py b/tasklib/backends.py index 12a9234..c99d63f 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -38,6 +38,10 @@ class Backend(object): def stop_task(self, task): pass + @abc.abstractmethod + def complete_task(self, task): + pass + @abc.abstractmethod def sync(self): """Syncs the backend database with the taskd server""" -- 2.39.5 From 15c5d693afb61c72f6a6984c25dacb8610b2d9a8 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 20:06:03 +0200 Subject: [PATCH 05/16] Task: Move TW-specific completion logic into TW backend --- tasklib/backends.py | 8 ++++++++ tasklib/task.py | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index c99d63f..e7b9a74 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -213,3 +213,11 @@ class TaskWarrior(object): 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 < VERSION_2_4_0 and task.active: + task.stop() + + self.execute_command([task['uuid'], 'done']) + diff --git a/tasklib/task.py b/tasklib/task.py index b7bba4b..3f53511 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -637,11 +637,7 @@ class Task(TaskResource): elif self.deleted: raise Task.DeletedTask("Deleted task cannot be completed") - # Older versions of TW do not stop active task at completion - if self.warrior.version < VERSION_2_4_0 and self.active: - self.stop() - - self.warrior.execute_command([self['uuid'], 'done']) + self.backend.complete_task(self) # Refresh the status again, so that we have updated info stored self.refresh(only_fields=['status', 'start', 'end']) -- 2.39.5 From 4a289416db9f24d81cacd14da40f5db196454e79 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 20:39:42 +0200 Subject: [PATCH 06/16] backends: Add refresh_task to the Backend interface --- tasklib/backends.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tasklib/backends.py b/tasklib/backends.py index e7b9a74..bf4cd67 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -42,6 +42,14 @@ class Backend(object): def complete_task(self, task): pass + @abc.abstractmethod + def refresh_task(self, task): + """ + Refreshes the given task. Returns new data dict with serialized + attributes. + """ + pass + @abc.abstractmethod def sync(self): """Syncs the backend database with the taskd server""" -- 2.39.5 From 398bbf824e6143cf760002af3065897011e469f0 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 20:44:01 +0200 Subject: [PATCH 07/16] Task: Move TW-specific refresh behaviour to TW backend --- tasklib/backends.py | 42 +++++++++++++++++++++++++++++++++++++++++- tasklib/task.py | 37 +------------------------------------ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index bf4cd67..92102cb 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -1,4 +1,5 @@ import abc +import json import os import re import subprocess @@ -43,7 +44,7 @@ class Backend(object): pass @abc.abstractmethod - def refresh_task(self, task): + def refresh_task(self, task, after_save=False): """ Refreshes the given task. Returns new data dict with serialized attributes. @@ -229,3 +230,42 @@ class TaskWarrior(object): self.execute_command([task['uuid'], 'done']) + 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 < 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 = TaskFilter(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]) + diff --git a/tasklib/task.py b/tasklib/task.py index 3f53511..5a86529 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -708,43 +708,8 @@ class Task(TaskResource): if not self.saved: raise Task.NotSaved("Task needs to be saved to be refreshed") - # 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 = [self['uuid'] or self['id'], 'export'] - output = self.warrior.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.warrior.version < 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(self._data) - data.pop('id', None) - data.pop('uuid', None) - - taskfilter = TaskFilter(self.warrior) - for key, value in data.items(): - taskfilter.add_filter_param(key, value) - - output = self.warrior.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( - self['uuid'] or self['id'], self['description'], output) - ) + new_data = self.backend.refresh_task(self, after_save=after_save) - new_data = json.loads(output[0]) if only_fields: to_update = dict( [(k, new_data.get(k)) for k in only_fields]) -- 2.39.5 From e52d223ba61ad4b121e91223ce33e860735ed6db Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 21:47:06 +0200 Subject: [PATCH 08/16] backends: Make filter classes backend-specific --- tasklib/backends.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index 92102cb..6aadf7a 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -4,6 +4,7 @@ import os import re import subprocess +from tasklib.task import TaskFilter VERSION_2_1_0 = six.u('2.1.0') VERSION_2_2_0 = six.u('2.2.0') @@ -18,6 +19,8 @@ VERSION_2_4_5 = six.u('2.4.5') class Backend(object): + filter_class = TaskFilter + @abc.abstractmethod def filter_tasks(self, filter_obj): """Returns a list of Task objects matching the given filter""" @@ -252,7 +255,7 @@ class TaskWarrior(object): data.pop('id', None) data.pop('uuid', None) - taskfilter = TaskFilter(self) + taskfilter = self.filter_class(self) for key, value in data.items(): taskfilter.add_filter_param(key, value) -- 2.39.5 From 71d228aa43ba3912ad252a9724b14fcb5a6cd12d Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 21:47:26 +0200 Subject: [PATCH 09/16] TaskWarrior: Implement sync method --- tasklib/backends.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasklib/backends.py b/tasklib/backends.py index 6aadf7a..80928f1 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -272,3 +272,5 @@ class TaskWarrior(object): return json.loads(output[0]) + def sync(self): + self.execute_command(['sync']) -- 2.39.5 From 0aa410df86d4a16b4117858a96147e896c2a7d35 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 21:57:06 +0200 Subject: [PATCH 10/16] backends: Add method for annotations to the backend interface --- tasklib/backends.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tasklib/backends.py b/tasklib/backends.py index 80928f1..a1d858f 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -54,6 +54,14 @@ class Backend(object): """ 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""" -- 2.39.5 From 1a429b9309cf543e937c9b8b061d0f99d1acffbe Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 22:16:48 +0200 Subject: [PATCH 11/16] backends: Move TW-specific annotation logic to TW backend --- tasklib/backends.py | 8 ++++++++ tasklib/task.py | 7 +++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index a1d858f..f0c73ef 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -241,6 +241,14 @@ class TaskWarrior(object): 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 diff --git a/tasklib/task.py b/tasklib/task.py index 5a86529..9ae63d4 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -653,8 +653,7 @@ class Task(TaskResource): if not self.saved: raise Task.NotSaved("Task needs to be saved to add annotation") - args = [self['uuid'], 'annotate', annotation] - self.warrior.execute_command(args) + self.backend.annotate_task(self, annotation) self.refresh(only_fields=['annotations']) def remove_annotation(self, annotation): @@ -663,8 +662,8 @@ class Task(TaskResource): if isinstance(annotation, TaskAnnotation): annotation = annotation['description'] - args = [self['uuid'], 'denotate', annotation] - self.warrior.execute_command(args) + + self.backend.denotate_task(self, annotation) self.refresh(only_fields=['annotations']) def _get_modified_fields_as_args(self): -- 2.39.5 From fda5cf272018a0596c3ea97c9d4658cf0b5fa048 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 23:22:38 +0200 Subject: [PATCH 12/16] Task: Conversion of modified fields to TW Cli args is TW-specific, move to backend --- tasklib/backends.py | 39 ++++++++++++++++++++++++++++++++++++++- tasklib/task.py | 36 ------------------------------------ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index f0c73ef..38b88b1 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -121,6 +121,43 @@ class TaskWarrior(object): 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: six.u("{0}:{1}").format(field, + escaped_serialized_value) + + format_func = getattr(task, 'format_{0}'.format(field), + format_default) + + args.append(format_func()) + + # 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(): + if field in task.read_only_fields: + continue + add_field(field) + + return args + + def get_config(self): raw_output = self.execute_command( ['show'], @@ -198,7 +235,7 @@ class TaskWarrior(object): """Save a task into TaskWarrior database using add/modify call""" args = [task['uuid'], 'modify'] if task.saved else ['add'] - args.extend(task._get_modified_fields_as_args()) + 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 diff --git a/tasklib/task.py b/tasklib/task.py index 9ae63d4..081646d 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -666,42 +666,6 @@ class Task(TaskResource): self.backend.denotate_task(self, annotation) self.refresh(only_fields=['annotations']) - def _get_modified_fields_as_args(self): - args = [] - - def add_field(field): - # Add the output of format_field method to args list (defaults to - # field:value) - serialized_value = self._serialize(field, self._data[field]) - - # 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: six.u("{0}:{1}").format(field, - escaped_serialized_value) - - format_func = getattr(self, 'format_{0}'.format(field), - format_default) - - args.append(format_func()) - - # If we're modifying saved task, simply pass on all modified fields - if self.saved: - for field in self._modified_fields: - add_field(field) - # For new tasks, pass all fields that make sense - else: - for field in self._data.keys(): - if field in self.read_only_fields: - continue - add_field(field) - - return args - 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: -- 2.39.5 From 6ca4e493312239436a5d8339741f22f1cd535883 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 23:28:02 +0200 Subject: [PATCH 13/16] Task: Move formatters to TaskWarrior backend --- tasklib/backends.py | 33 +++++++++++++++++++++++++++++++-- tasklib/task.py | 27 --------------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index 38b88b1..02d412c 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -139,10 +139,10 @@ class TaskWarrior(object): format_default = lambda: six.u("{0}:{1}").format(field, escaped_serialized_value) - format_func = getattr(task, 'format_{0}'.format(field), + format_func = getattr(self, 'format_{0}'.format(field), format_default) - args.append(format_func()) + args.append(format_func(task)) # If we're modifying saved task, simply pass on all modified fields if task.saved: @@ -157,6 +157,35 @@ class TaskWarrior(object): 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 = self['depends'] - old_dependencies + removed = old_dependencies - self['depends'] + + # Removed dependencies need to be prefixed with '-' + return 'depends:' + ','.join( + [t['uuid'] for t in added] + + ['-' + t['uuid'] for t in removed] + ) + + def format_description(self, task): + # Task version older than 2.4.0 ignores first word of the + # task description if description: prefix is used + if self.version < VERSION_2_4_0: + return task._data['description'] + else: + return six.u("description:'{0}'").format(task._data['description'] or '') + + + # Public interface def get_config(self): raw_output = self.execute_command( diff --git a/tasklib/task.py b/tasklib/task.py index 081646d..211573d 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -549,33 +549,6 @@ class Task(TaskResource): return super(Task, self).serialize_depends(cur_dependencies) - def format_depends(self): - # 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 = self._original_data.get('depends', set()) - - added = self['depends'] - old_dependencies - removed = old_dependencies - self['depends'] - - # Removed dependencies need to be prefixed with '-' - return 'depends:' + ','.join( - [t['uuid'] for t in added] + - ['-' + t['uuid'] for t in removed] - ) - - def format_description(self): - # Task version older than 2.4.0 ignores first word of the - # task description if description: prefix is used - if self.warrior.version < VERSION_2_4_0: - return self._data['description'] - else: - return six.u("description:'{0}'").format(self._data['description'] or '') - def delete(self): if not self.saved: raise Task.NotSaved("Task needs to be saved before it can be deleted") -- 2.39.5 From 5ba0e1105517f8b0e22b0bcf5c79f40a0fb644d9 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 23:38:22 +0200 Subject: [PATCH 14/16] filters: Move TaskFilter into separate module --- tasklib/filters.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ tasklib/task.py | 49 --------------------------------------------- 2 files changed, 50 insertions(+), 49 deletions(-) create mode 100644 tasklib/filters.py diff --git a/tasklib/filters.py b/tasklib/filters.py new file mode 100644 index 0000000..1899d1d --- /dev/null +++ b/tasklib/filters.py @@ -0,0 +1,50 @@ +from tasklib.serializing import SerializingObject + + +class TaskFilter(SerializingObject): + """ + A set of parameters to filter the task list with. + """ + + def __init__(self, warrior, filter_params=None): + self.filter_params = filter_params or [] + super(TaskFilter, self).__init__(warrior) + + 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.warrior.version < 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.warrior) + c.filter_params = list(self.filter_params) + return c diff --git a/tasklib/task.py b/tasklib/task.py index 211573d..614fd56 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -653,55 +653,6 @@ class Task(TaskResource): else: self._load_data(new_data) -class TaskFilter(SerializingObject): - """ - A set of parameters to filter the task list with. - """ - - def __init__(self, warrior, filter_params=None): - self.filter_params = filter_params or [] - super(TaskFilter, self).__init__(warrior) - - 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.warrior.version < 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.warrior) - c.filter_params = list(self.filter_params) - return c - - class TaskQuerySet(object): """ Represents a lazy lookup for a task objects. -- 2.39.5 From ff45f9122b3f1a441491d9bcb0d9681039baea38 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 23:52:49 +0200 Subject: [PATCH 15/16] TaskFilter: Rename TaskFilter to TaskWarriorFilter --- tasklib/backends.py | 4 ++-- tasklib/filters.py | 2 +- tasklib/task.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tasklib/backends.py b/tasklib/backends.py index 02d412c..51285b5 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -4,7 +4,7 @@ import os import re import subprocess -from tasklib.task import TaskFilter +from tasklib.filters import TaskWarriorFilter VERSION_2_1_0 = six.u('2.1.0') VERSION_2_2_0 = six.u('2.2.0') @@ -19,7 +19,7 @@ VERSION_2_4_5 = six.u('2.4.5') class Backend(object): - filter_class = TaskFilter + filter_class = TaskWarriorFilter @abc.abstractmethod def filter_tasks(self, filter_obj): diff --git a/tasklib/filters.py b/tasklib/filters.py index 1899d1d..211302f 100644 --- a/tasklib/filters.py +++ b/tasklib/filters.py @@ -1,7 +1,7 @@ from tasklib.serializing import SerializingObject -class TaskFilter(SerializingObject): +class TaskWarriorFilter(SerializingObject): """ A set of parameters to filter the task list with. """ diff --git a/tasklib/task.py b/tasklib/task.py index 614fd56..d39c10c 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -54,7 +54,7 @@ class ReadOnlyDictView(object): class SerializingObject(object): """ - Common ancestor for TaskResource & TaskFilter, since they both + Common ancestor for TaskResource & TaskWarriorFilter, since they both need to serialize arguments. Serializing method should hold the following contract: @@ -661,7 +661,7 @@ class TaskQuerySet(object): def __init__(self, warrior=None, filter_obj=None): self.warrior = warrior self._result_cache = None - self.filter_obj = filter_obj or TaskFilter(warrior) + self.filter_obj = filter_obj or TaskWarriorFilter(warrior) def __deepcopy__(self, memo): """ -- 2.39.5 From f986b814683ddee836b534f344bca017ac697c58 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Sat, 8 Aug 2015 23:55:47 +0200 Subject: [PATCH 16/16] filters: Define TaskFilter base class --- tasklib/filters.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tasklib/filters.py b/tasklib/filters.py index 211302f..7b44e98 100644 --- a/tasklib/filters.py +++ b/tasklib/filters.py @@ -1,7 +1,35 @@ +import abc from tasklib.serializing import SerializingObject -class TaskWarriorFilter(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. """ -- 2.39.5