All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 from __future__ import print_function
13 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
14 DATE_FORMAT_CALC = '%Y-%m-%dT%H:%M:%S'
17 COMPLETED = 'completed'
19 VERSION_2_1_0 = six.u('2.1.0')
20 VERSION_2_2_0 = six.u('2.2.0')
21 VERSION_2_3_0 = six.u('2.3.0')
22 VERSION_2_4_0 = six.u('2.4.0')
23 VERSION_2_4_1 = six.u('2.4.1')
24 VERSION_2_4_2 = six.u('2.4.2')
25 VERSION_2_4_3 = six.u('2.4.3')
27 logger = logging.getLogger(__name__)
28 local_zone = tzlocal.get_localzone()
31 class TaskWarriorException(Exception):
35 class ReadOnlyDictView(object):
37 Provides simplified read-only view upon dict object.
40 def __init__(self, viewed_dict):
41 self.viewed_dict = viewed_dict
43 def __getitem__(self, key):
44 return copy.deepcopy(self.viewed_dict.__getitem__(key))
46 def __contains__(self, k):
47 return self.viewed_dict.__contains__(k)
50 for value in self.viewed_dict:
51 yield copy.deepcopy(value)
54 return len(self.viewed_dict)
56 def get(self, key, default=None):
57 return copy.deepcopy(self.viewed_dict.get(key, default))
60 return [copy.deepcopy(v) for v in self.viewed_dict.items()]
63 return [copy.deepcopy(v) for v in self.viewed_dict.values()]
66 class SerializingObject(object):
68 Common ancestor for TaskResource & TaskFilter, since they both
69 need to serialize arguments.
71 Serializing method should hold the following contract:
72 - any empty value (meaning removal of the attribute)
73 is deserialized into a empty string
74 - None denotes a empty value for any attribute
76 Deserializing method should hold the following contract:
77 - None denotes a empty value for any attribute (however,
78 this is here as a safeguard, TaskWarrior currently does
79 not export empty-valued attributes) if the attribute
80 is not iterable (e.g. list or set), in which case
81 a empty iterable should be used.
83 Normalizing methods should hold the following contract:
84 - They are used to validate and normalize the user input.
85 Any attribute value that comes from the user (during Task
86 initialization, assignign values to Task attributes, or
87 filtering by user-provided values of attributes) is first
88 validated and normalized using the normalize_{key} method.
89 - If validation or normalization fails, normalizer is expected
93 def __init__(self, warrior):
94 self.warrior = warrior
96 def _deserialize(self, key, value):
97 hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
98 lambda x: x if x != '' else None)
99 return hydrate_func(value)
101 def _serialize(self, key, value):
102 dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
103 lambda x: x if x is not None else '')
104 return dehydrate_func(value)
106 def _normalize(self, key, value):
108 Use normalize_<key> methods to normalize user input. Any user
109 input will be normalized at the moment it is used as filter,
110 or entered as a value of Task attribute.
113 # None value should not be converted by normalizer
117 normalize_func = getattr(self, 'normalize_{0}'.format(key),
120 return normalize_func(value)
122 def timestamp_serializer(self, date):
126 # Any serialized timestamp should be localized, we need to
127 # convert to UTC before converting to string (DATE_FORMAT uses UTC)
128 date = date.astimezone(pytz.utc)
130 return date.strftime(DATE_FORMAT)
132 def timestamp_deserializer(self, date_str):
136 # Return timestamp localized in the local zone
137 naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
138 localized_timestamp = pytz.utc.localize(naive_timestamp)
139 return localized_timestamp.astimezone(local_zone)
141 def serialize_entry(self, value):
142 return self.timestamp_serializer(value)
144 def deserialize_entry(self, value):
145 return self.timestamp_deserializer(value)
147 def normalize_entry(self, value):
148 return self.datetime_normalizer(value)
150 def serialize_modified(self, value):
151 return self.timestamp_serializer(value)
153 def deserialize_modified(self, value):
154 return self.timestamp_deserializer(value)
156 def normalize_modified(self, value):
157 return self.datetime_normalizer(value)
159 def serialize_start(self, value):
160 return self.timestamp_serializer(value)
162 def deserialize_start(self, value):
163 return self.timestamp_deserializer(value)
165 def normalize_start(self, value):
166 return self.datetime_normalizer(value)
168 def serialize_end(self, value):
169 return self.timestamp_serializer(value)
171 def deserialize_end(self, value):
172 return self.timestamp_deserializer(value)
174 def normalize_end(self, value):
175 return self.datetime_normalizer(value)
177 def serialize_due(self, value):
178 return self.timestamp_serializer(value)
180 def deserialize_due(self, value):
181 return self.timestamp_deserializer(value)
183 def normalize_due(self, value):
184 return self.datetime_normalizer(value)
186 def serialize_scheduled(self, value):
187 return self.timestamp_serializer(value)
189 def deserialize_scheduled(self, value):
190 return self.timestamp_deserializer(value)
192 def normalize_scheduled(self, value):
193 return self.datetime_normalizer(value)
195 def serialize_until(self, value):
196 return self.timestamp_serializer(value)
198 def deserialize_until(self, value):
199 return self.timestamp_deserializer(value)
201 def normalize_until(self, value):
202 return self.datetime_normalizer(value)
204 def serialize_wait(self, value):
205 return self.timestamp_serializer(value)
207 def deserialize_wait(self, value):
208 return self.timestamp_deserializer(value)
210 def normalize_wait(self, value):
211 return self.datetime_normalizer(value)
213 def serialize_annotations(self, value):
214 value = value if value is not None else []
216 # This may seem weird, but it's correct, we want to export
217 # a list of dicts as serialized value
218 serialized_annotations = [json.loads(annotation.export_data())
219 for annotation in value]
220 return serialized_annotations if serialized_annotations else ''
222 def deserialize_annotations(self, data):
223 return [TaskAnnotation(self, d) for d in data] if data else []
225 def serialize_tags(self, tags):
226 return ','.join(tags) if tags else ''
228 def deserialize_tags(self, tags):
229 if isinstance(tags, six.string_types):
230 return tags.split(',') if tags else []
233 def serialize_depends(self, value):
234 # Return the list of uuids
235 value = value if value is not None else set()
236 return ','.join(task['uuid'] for task in value)
238 def deserialize_depends(self, raw_uuids):
239 raw_uuids = raw_uuids or '' # Convert None to empty string
240 uuids = raw_uuids.split(',')
241 return set(self.warrior.tasks.get(uuid=uuid) for uuid in uuids if uuid)
243 def datetime_normalizer(self, value):
245 Normalizes date/datetime value (considered to come from user input)
246 to localized datetime value. Following conversions happen:
248 naive date -> localized datetime with the same date, and time=midnight
249 naive datetime -> localized datetime with the same value
250 localized datetime -> localized datetime (no conversion)
253 if (isinstance(value, datetime.date)
254 and not isinstance(value, datetime.datetime)):
255 # Convert to local midnight
256 value_full = datetime.datetime.combine(value, datetime.time.min)
257 localized = local_zone.localize(value_full)
258 elif isinstance(value, datetime.datetime):
259 if value.tzinfo is None:
260 # Convert to localized datetime object
261 localized = local_zone.localize(value)
263 # If the value is already localized, there is no need to change
264 # time zone at this point. Also None is a valid value too.
266 elif (isinstance(value, six.string_types)
267 and self.warrior.version >= VERSION_2_4_0):
268 # For strings, use 'task calc' to evaluate the string to datetime
269 # available since TW 2.4.0
271 result = self.warrior.execute_command(['calc'] + args)
272 naive = datetime.datetime.strptime(result[0], DATE_FORMAT_CALC)
273 localized = local_zone.localize(naive)
275 raise ValueError("Provided value could not be converted to "
276 "datetime, its type is not supported: {}"
277 .format(type(value)))
281 def normalize_uuid(self, value):
283 if not isinstance(value, six.string_types) or value == '':
284 raise ValueError("UUID must be a valid non-empty string, "
285 "not: {}".format(value))
290 class TaskResource(SerializingObject):
291 read_only_fields = []
293 def _load_data(self, data):
294 self._data = dict((key, self._deserialize(key, value))
295 for key, value in data.items())
296 # We need to use a copy for original data, so that changes
297 # are not propagated.
298 self._original_data = copy.deepcopy(self._data)
300 def _update_data(self, data, update_original=False, remove_missing=False):
302 Low level update of the internal _data dict. Data which are coming as
303 updates should already be serialized. If update_original is True, the
304 original_data dict is updated as well.
306 self._data.update(dict((key, self._deserialize(key, value))
307 for key, value in data.items()))
309 # In certain situations, we want to treat missing keys as removals
311 for key in set(self._data.keys()) - set(data.keys()):
312 self._data[key] = None
315 self._original_data = copy.deepcopy(self._data)
318 def __getitem__(self, key):
319 # This is a workaround to make TaskResource non-iterable
320 # over simple index-based iteration
327 if key not in self._data:
328 self._data[key] = self._deserialize(key, None)
330 return self._data.get(key)
332 def __setitem__(self, key, value):
333 if key in self.read_only_fields:
334 raise RuntimeError('Field \'%s\' is read-only' % key)
336 # Normalize the user input before saving it
337 value = self._normalize(key, value)
338 self._data[key] = value
341 s = six.text_type(self.__unicode__())
343 s = s.encode('utf-8')
349 def export_data(self):
351 Exports current data contained in the Task as JSON
354 # We need to remove spaces for TW-1504, use custom separators
355 data_tuples = ((key, self._serialize(key, value))
356 for key, value in six.iteritems(self._data))
358 # Empty string denotes empty serialized value, we do not want
359 # to pass that to TaskWarrior.
360 data_tuples = filter(lambda t: t[1] is not '', data_tuples)
361 data = dict(data_tuples)
362 return json.dumps(data, separators=(',',':'))
365 def _modified_fields(self):
366 writable_fields = set(self._data.keys()) - set(self.read_only_fields)
367 for key in writable_fields:
368 new_value = self._data.get(key)
369 old_value = self._original_data.get(key)
371 # Make sure not to mark data removal as modified field if the
372 # field originally had some empty value
373 if key in self._data and not new_value and not old_value:
376 if new_value != old_value:
381 return bool(list(self._modified_fields))
384 class TaskAnnotation(TaskResource):
385 read_only_fields = ['entry', 'description']
387 def __init__(self, task, data={}):
389 self._load_data(data)
390 super(TaskAnnotation, self).__init__(task.warrior)
393 self.task.remove_annotation(self)
395 def __unicode__(self):
396 return self['description']
398 def __eq__(self, other):
399 # consider 2 annotations equal if they belong to the same task, and
400 # their data dics are the same
401 return self.task == other.task and self._data == other._data
403 __repr__ = __unicode__
406 class Task(TaskResource):
407 read_only_fields = ['id', 'entry', 'urgency', 'uuid', 'modified']
409 class DoesNotExist(Exception):
412 class CompletedTask(Exception):
414 Raised when the operation cannot be performed on the completed task.
418 class DeletedTask(Exception):
420 Raised when the operation cannot be performed on the deleted task.
424 class InactiveTask(Exception):
426 Raised when the operation cannot be performed on an inactive task.
430 class NotSaved(Exception):
432 Raised when the operation cannot be performed on the task, because
433 it has not been saved to TaskWarrior yet.
438 def from_input(cls, input_file=sys.stdin, modify=None, warrior=None):
440 Creates a Task object, directly from the stdin, by reading one line.
441 If modify=True, two lines are used, first line interpreted as the
442 original state of the Task object, and second line as its new,
443 modified value. This is consistent with the TaskWarrior's hook
446 Object created by this method should not be saved, deleted
447 or refreshed, as t could create a infinite loop. For this
448 reason, TaskWarrior instance is set to None.
450 Input_file argument can be used to specify the input file,
451 but defaults to sys.stdin.
454 # Detect the hook type if not given directly
455 name = os.path.basename(sys.argv[0])
456 modify = name.startswith('on-modify') if modify is None else modify
458 # Create the TaskWarrior instance if none passed
460 hook_parent_dir = os.path.dirname(os.path.dirname(sys.argv[0]))
461 warrior = TaskWarrior(data_location=hook_parent_dir)
463 # TaskWarrior instance is set to None
466 # Load the data from the input
467 task._load_data(json.loads(input_file.readline().strip()))
469 # If this is a on-modify event, we are provided with additional
470 # line of input, which provides updated data
472 task._update_data(json.loads(input_file.readline().strip()),
477 def __init__(self, warrior, **kwargs):
478 super(Task, self).__init__(warrior)
480 # Check that user is not able to set read-only value in __init__
481 for key in kwargs.keys():
482 if key in self.read_only_fields:
483 raise RuntimeError('Field \'%s\' is read-only' % key)
485 # We serialize the data in kwargs so that users of the library
486 # do not have to pass different data formats via __setitem__ and
487 # __init__ methods, that would be confusing
489 # Rather unfortunate syntax due to python2.6 comaptiblity
490 self._data = dict((key, self._normalize(key, value))
491 for (key, value) in six.iteritems(kwargs))
492 self._original_data = copy.deepcopy(self._data)
494 # Provide read only access to the original data
495 self.original = ReadOnlyDictView(self._original_data)
497 def __unicode__(self):
498 return self['description']
500 def __eq__(self, other):
501 if self['uuid'] and other['uuid']:
502 # For saved Tasks, just define equality by equality of uuids
503 return self['uuid'] == other['uuid']
505 # If the tasks are not saved, compare the actual instances
506 return id(self) == id(other)
511 # For saved Tasks, just define equality by equality of uuids
512 return self['uuid'].__hash__()
514 # If the tasks are not saved, return hash of instance id
515 return id(self).__hash__()
519 return self['status'] == six.text_type('completed')
523 return self['status'] == six.text_type('deleted')
527 return self['status'] == six.text_type('waiting')
531 return self['status'] == six.text_type('pending')
535 return self['start'] is not None
539 return self['uuid'] is not None or self['id'] is not None
541 def serialize_depends(self, cur_dependencies):
542 # Check that all the tasks are saved
543 for task in (cur_dependencies or set()):
545 raise Task.NotSaved('Task \'%s\' needs to be saved before '
546 'it can be set as dependency.' % task)
548 return super(Task, self).serialize_depends(cur_dependencies)
550 def format_depends(self):
551 # We need to generate added and removed dependencies list,
552 # since Taskwarrior does not accept redefining dependencies.
554 # This cannot be part of serialize_depends, since we need
555 # to keep a list of all depedencies in the _data dictionary,
556 # not just currently added/removed ones
558 old_dependencies = self._original_data.get('depends', set())
560 added = self['depends'] - old_dependencies
561 removed = old_dependencies - self['depends']
563 # Removed dependencies need to be prefixed with '-'
564 return 'depends:' + ','.join(
565 [t['uuid'] for t in added] +
566 ['-' + t['uuid'] for t in removed]
569 def format_description(self):
570 # Task version older than 2.4.0 ignores first word of the
571 # task description if description: prefix is used
572 if self.warrior.version < VERSION_2_4_0:
573 return self._data['description']
575 return six.u("description:'{0}'").format(self._data['description'] or '')
579 raise Task.NotSaved("Task needs to be saved before it can be deleted")
581 # Refresh the status, and raise exception if the task is deleted
582 self.refresh(only_fields=['status'])
585 raise Task.DeletedTask("Task was already deleted")
587 self.warrior.execute_command([self['uuid'], 'delete'])
589 # Refresh the status again, so that we have updated info stored
590 self.refresh(only_fields=['status', 'start', 'end'])
594 raise Task.NotSaved("Task needs to be saved before it can be started")
596 # Refresh, and raise exception if task is already completed/deleted
597 self.refresh(only_fields=['status'])
600 raise Task.CompletedTask("Cannot start a completed task")
602 raise Task.DeletedTask("Deleted task cannot be started")
604 self.warrior.execute_command([self['uuid'], 'start'])
606 # Refresh the status again, so that we have updated info stored
607 self.refresh(only_fields=['status', 'start'])
611 raise Task.NotSaved("Task needs to be saved before it can be stopped")
613 # Refresh, and raise exception if task is already completed/deleted
614 self.refresh(only_fields=['status'])
617 raise Task.InactiveTask("Cannot stop an inactive task")
619 self.warrior.execute_command([self['uuid'], 'stop'])
621 # Refresh the status again, so that we have updated info stored
622 self.refresh(only_fields=['status', 'start'])
626 raise Task.NotSaved("Task needs to be saved before it can be completed")
628 # Refresh, and raise exception if task is already completed/deleted
629 self.refresh(only_fields=['status'])
632 raise Task.CompletedTask("Cannot complete a completed task")
634 raise Task.DeletedTask("Deleted task cannot be completed")
636 # Older versions of TW do not stop active task at completion
637 if self.warrior.version < VERSION_2_4_0 and self.active:
640 self.warrior.execute_command([self['uuid'], 'done'])
642 # Refresh the status again, so that we have updated info stored
643 self.refresh(only_fields=['status', 'start', 'end'])
646 if self.saved and not self.modified:
649 args = [self['uuid'], 'modify'] if self.saved else ['add']
650 args.extend(self._get_modified_fields_as_args())
651 output = self.warrior.execute_command(args)
653 # Parse out the new ID, if the task is being added for the first time
655 id_lines = [l for l in output if l.startswith('Created task ')]
657 # Complain loudly if it seems that more tasks were created
659 if len(id_lines) != 1 or len(id_lines[0].split(' ')) != 3:
660 raise TaskWarriorException("Unexpected output when creating "
661 "task: %s" % '\n'.join(id_lines))
663 # Circumvent the ID storage, since ID is considered read-only
664 self._data['id'] = int(id_lines[0].split(' ')[2].rstrip('.'))
666 # Refreshing is very important here, as not only modification time
667 # is updated, but arbitrary attribute may have changed due hooks
668 # altering the data before saving
671 def add_annotation(self, annotation):
673 raise Task.NotSaved("Task needs to be saved to add annotation")
675 args = [self['uuid'], 'annotate', annotation]
676 self.warrior.execute_command(args)
677 self.refresh(only_fields=['annotations'])
679 def remove_annotation(self, annotation):
681 raise Task.NotSaved("Task needs to be saved to remove annotation")
683 if isinstance(annotation, TaskAnnotation):
684 annotation = annotation['description']
685 args = [self['uuid'], 'denotate', annotation]
686 self.warrior.execute_command(args)
687 self.refresh(only_fields=['annotations'])
689 def _get_modified_fields_as_args(self):
692 def add_field(field):
693 # Add the output of format_field method to args list (defaults to
695 serialized_value = self._serialize(field, self._data[field])
697 # Empty values should not be enclosed in quotation marks, see
699 if serialized_value is '':
700 escaped_serialized_value = ''
702 escaped_serialized_value = six.u("'{0}'").format(serialized_value)
704 format_default = lambda: six.u("{0}:{1}").format(field,
705 escaped_serialized_value)
707 format_func = getattr(self, 'format_{0}'.format(field),
710 args.append(format_func())
712 # If we're modifying saved task, simply pass on all modified fields
714 for field in self._modified_fields:
716 # For new tasks, pass all fields that make sense
718 for field in self._data.keys():
719 if field in self.read_only_fields:
725 def refresh(self, only_fields=[]):
726 # Raise error when trying to refresh a task that has not been saved
728 raise Task.NotSaved("Task needs to be saved to be refreshed")
730 # We need to use ID as backup for uuid here for the refreshes
731 # of newly saved tasks. Any other place in the code is fine
732 # with using UUID only.
733 args = [self['uuid'] or self['id'], 'export']
734 new_data = json.loads(self.warrior.execute_command(args)[0])
737 [(k, new_data.get(k)) for k in only_fields])
738 self._update_data(to_update, update_original=True)
740 self._load_data(new_data)
742 class TaskFilter(SerializingObject):
744 A set of parameters to filter the task list with.
747 def __init__(self, warrior, filter_params=[]):
748 self.filter_params = filter_params
749 super(TaskFilter, self).__init__(warrior)
751 def add_filter(self, filter_str):
752 self.filter_params.append(filter_str)
754 def add_filter_param(self, key, value):
755 key = key.replace('__', '.')
757 # Replace the value with empty string, since that is the
758 # convention in TW for empty values
759 attribute_key = key.split('.')[0]
761 # Since this is user input, we need to normalize before we serialize
762 value = self._normalize(attribute_key, value)
763 value = self._serialize(attribute_key, value)
765 # If we are filtering by uuid:, do not use uuid keyword
768 self.filter_params.insert(0, value)
770 # Surround value with aphostrophes unless it's a empty string
771 value = "'%s'" % value if value else ''
773 # We enforce equality match by using 'is' (or 'none') modifier
774 # Without using this syntax, filter fails due to TW-1479
775 modifier = '.is' if value else '.none'
776 key = key + modifier if '.' not in key else key
778 self.filter_params.append(six.u("{0}:{1}").format(key, value))
780 def get_filter_params(self):
781 return [f for f in self.filter_params if f]
784 c = self.__class__(self.warrior)
785 c.filter_params = list(self.filter_params)
789 class TaskQuerySet(object):
791 Represents a lazy lookup for a task objects.
794 def __init__(self, warrior=None, filter_obj=None):
795 self.warrior = warrior
796 self._result_cache = None
797 self.filter_obj = filter_obj or TaskFilter(warrior)
799 def __deepcopy__(self, memo):
801 Deep copy of a QuerySet doesn't populate the cache
803 obj = self.__class__()
804 for k, v in self.__dict__.items():
805 if k in ('_iter', '_result_cache'):
806 obj.__dict__[k] = None
808 obj.__dict__[k] = copy.deepcopy(v, memo)
812 data = list(self[:REPR_OUTPUT_SIZE + 1])
813 if len(data) > REPR_OUTPUT_SIZE:
814 data[-1] = "...(remaining elements truncated)..."
818 if self._result_cache is None:
819 self._result_cache = list(self)
820 return len(self._result_cache)
823 if self._result_cache is None:
824 self._result_cache = self._execute()
825 return iter(self._result_cache)
827 def __getitem__(self, k):
828 if self._result_cache is None:
829 self._result_cache = list(self)
830 return self._result_cache.__getitem__(k)
833 if self._result_cache is not None:
834 return bool(self._result_cache)
837 except StopIteration:
841 def __nonzero__(self):
842 return type(self).__bool__(self)
844 def _clone(self, klass=None, **kwargs):
846 klass = self.__class__
847 filter_obj = self.filter_obj.clone()
848 c = klass(warrior=self.warrior, filter_obj=filter_obj)
849 c.__dict__.update(kwargs)
854 Fetch the tasks which match the current filters.
856 return self.warrior.filter_tasks(self.filter_obj)
860 Returns a new TaskQuerySet that is a copy of the current one.
865 return self.filter(status=PENDING)
868 return self.filter(status=COMPLETED)
870 def filter(self, *args, **kwargs):
872 Returns a new TaskQuerySet with the given filters added.
874 clone = self._clone()
876 clone.filter_obj.add_filter(f)
877 for key, value in kwargs.items():
878 clone.filter_obj.add_filter_param(key, value)
881 def get(self, **kwargs):
883 Performs the query and returns a single object matching the given
886 clone = self.filter(**kwargs)
889 return clone._result_cache[0]
891 raise Task.DoesNotExist(
892 'Task matching query does not exist. '
893 'Lookup parameters were {0}'.format(kwargs))
895 'get() returned more than one Task -- it returned {0}! '
896 'Lookup parameters were {1}'.format(num, kwargs))
899 class TaskWarrior(object):
900 def __init__(self, data_location=None, create=True, taskrc_location='~/.taskrc'):
901 self.taskrc_location = os.path.expanduser(taskrc_location)
903 # If taskrc does not exist, pass / to use defaults and avoid creating
904 # dummy .taskrc file by TaskWarrior
905 if not os.path.exists(self.taskrc_location):
906 self.taskrc_location = '/'
908 self.version = self._get_version()
910 'confirmation': 'no',
911 'dependency.confirmation': 'no', # See TW-1483 or taskrc man page
912 'recurrence.confirmation': 'no', # Necessary for modifying R tasks
913 # 2.4.3 onwards supports 0 as infite bulk, otherwise set just
914 # arbitrary big number which is likely to be large enough
915 'bulk': 0 if self.version >= VERSION_2_4_3 else 100000,
918 # Set data.location override if passed via kwarg
919 if data_location is not None:
920 data_location = os.path.expanduser(data_location)
921 if create and not os.path.exists(data_location):
922 os.makedirs(data_location)
923 self.config['data.location'] = data_location
925 self.tasks = TaskQuerySet(self)
927 def _get_command_args(self, args, config_override={}):
928 command_args = ['task', 'rc:{0}'.format(self.taskrc_location)]
929 config = self.config.copy()
930 config.update(config_override)
931 for item in config.items():
932 command_args.append('rc.{0}={1}'.format(*item))
933 command_args.extend(map(six.text_type, args))
936 def _get_version(self):
937 p = subprocess.Popen(
938 ['task', '--version'],
939 stdout=subprocess.PIPE,
940 stderr=subprocess.PIPE)
941 stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
942 return stdout.strip('\n')
944 def execute_command(self, args, config_override={}, allow_failure=True,
946 command_args = self._get_command_args(
947 args, config_override=config_override)
948 logger.debug(' '.join(command_args))
949 p = subprocess.Popen(command_args, stdout=subprocess.PIPE,
950 stderr=subprocess.PIPE)
951 stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
952 if p.returncode and allow_failure:
954 error_msg = stderr.strip()
956 error_msg = stdout.strip()
957 raise TaskWarriorException(error_msg)
959 # Return all whole triplet only if explicitly asked for
961 return stdout.rstrip().split('\n')
963 return (stdout.rstrip().split('\n'),
964 stderr.rstrip().split('\n'),
967 def enforce_recurrence(self):
968 # Run arbitrary report command which will trigger generation
969 # of recurrent tasks.
971 # Only necessary for TW up to 2.4.1, fixed in 2.4.2.
972 if self.version < VERSION_2_4_2:
973 self.execute_command(['next'], allow_failure=False)
975 def filter_tasks(self, filter_obj):
976 self.enforce_recurrence()
977 args = ['export', '--'] + filter_obj.get_filter_params()
979 for line in self.execute_command(args):
981 data = line.strip(',')
983 filtered_task = Task(self)
984 filtered_task._load_data(json.loads(data))
985 tasks.append(filtered_task)
987 raise TaskWarriorException('Invalid JSON: %s' % data)
990 def merge_with(self, path, push=False):
991 path = path.rstrip('/') + '/'
992 self.execute_command(['merge', path], config_override={
993 'merge.autopush': 'yes' if push else 'no',
997 self.execute_command(['undo'])