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
14 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
15 DATE_FORMAT_CALC = '%Y-%m-%dT%H:%M:%S'
18 COMPLETED = 'completed'
20 VERSION_2_1_0 = six.u('2.1.0')
21 VERSION_2_2_0 = six.u('2.2.0')
22 VERSION_2_3_0 = six.u('2.3.0')
23 VERSION_2_4_0 = six.u('2.4.0')
24 VERSION_2_4_1 = six.u('2.4.1')
25 VERSION_2_4_2 = six.u('2.4.2')
26 VERSION_2_4_3 = six.u('2.4.3')
28 logger = logging.getLogger(__name__)
29 local_zone = tzlocal.get_localzone()
32 class TaskWarriorException(Exception):
36 class ReadOnlyDictView(object):
38 Provides simplified read-only view upon dict object.
41 def __init__(self, viewed_dict):
42 self.viewed_dict = viewed_dict
44 def __getitem__(self, key):
45 return copy.deepcopy(self.viewed_dict.__getitem__(key))
47 def __contains__(self, k):
48 return self.viewed_dict.__contains__(k)
51 for value in self.viewed_dict:
52 yield copy.deepcopy(value)
55 return len(self.viewed_dict)
57 def get(self, key, default=None):
58 return copy.deepcopy(self.viewed_dict.get(key, default))
61 return [copy.deepcopy(v) for v in self.viewed_dict.items()]
64 return [copy.deepcopy(v) for v in self.viewed_dict.values()]
67 class SerializingObject(object):
69 Common ancestor for TaskResource & TaskFilter, since they both
70 need to serialize arguments.
72 Serializing method should hold the following contract:
73 - any empty value (meaning removal of the attribute)
74 is deserialized into a empty string
75 - None denotes a empty value for any attribute
77 Deserializing method should hold the following contract:
78 - None denotes a empty value for any attribute (however,
79 this is here as a safeguard, TaskWarrior currently does
80 not export empty-valued attributes) if the attribute
81 is not iterable (e.g. list or set), in which case
82 a empty iterable should be used.
84 Normalizing methods should hold the following contract:
85 - They are used to validate and normalize the user input.
86 Any attribute value that comes from the user (during Task
87 initialization, assignign values to Task attributes, or
88 filtering by user-provided values of attributes) is first
89 validated and normalized using the normalize_{key} method.
90 - If validation or normalization fails, normalizer is expected
94 def __init__(self, warrior):
95 self.warrior = warrior
97 def _deserialize(self, key, value):
98 hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
99 lambda x: x if x != '' else None)
100 return hydrate_func(value)
102 def _serialize(self, key, value):
103 dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
104 lambda x: x if x is not None else '')
105 return dehydrate_func(value)
107 def _normalize(self, key, value):
109 Use normalize_<key> methods to normalize user input. Any user
110 input will be normalized at the moment it is used as filter,
111 or entered as a value of Task attribute.
114 # None value should not be converted by normalizer
118 normalize_func = getattr(self, 'normalize_{0}'.format(key),
121 return normalize_func(value)
123 def timestamp_serializer(self, date):
127 # Any serialized timestamp should be localized, we need to
128 # convert to UTC before converting to string (DATE_FORMAT uses UTC)
129 date = date.astimezone(pytz.utc)
131 return date.strftime(DATE_FORMAT)
133 def timestamp_deserializer(self, date_str):
137 # Return timestamp localized in the local zone
138 naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
139 localized_timestamp = pytz.utc.localize(naive_timestamp)
140 return localized_timestamp.astimezone(local_zone)
142 def serialize_entry(self, value):
143 return self.timestamp_serializer(value)
145 def deserialize_entry(self, value):
146 return self.timestamp_deserializer(value)
148 def normalize_entry(self, value):
149 return self.datetime_normalizer(value)
151 def serialize_modified(self, value):
152 return self.timestamp_serializer(value)
154 def deserialize_modified(self, value):
155 return self.timestamp_deserializer(value)
157 def normalize_modified(self, value):
158 return self.datetime_normalizer(value)
160 def serialize_start(self, value):
161 return self.timestamp_serializer(value)
163 def deserialize_start(self, value):
164 return self.timestamp_deserializer(value)
166 def normalize_start(self, value):
167 return self.datetime_normalizer(value)
169 def serialize_end(self, value):
170 return self.timestamp_serializer(value)
172 def deserialize_end(self, value):
173 return self.timestamp_deserializer(value)
175 def normalize_end(self, value):
176 return self.datetime_normalizer(value)
178 def serialize_due(self, value):
179 return self.timestamp_serializer(value)
181 def deserialize_due(self, value):
182 return self.timestamp_deserializer(value)
184 def normalize_due(self, value):
185 return self.datetime_normalizer(value)
187 def serialize_scheduled(self, value):
188 return self.timestamp_serializer(value)
190 def deserialize_scheduled(self, value):
191 return self.timestamp_deserializer(value)
193 def normalize_scheduled(self, value):
194 return self.datetime_normalizer(value)
196 def serialize_until(self, value):
197 return self.timestamp_serializer(value)
199 def deserialize_until(self, value):
200 return self.timestamp_deserializer(value)
202 def normalize_until(self, value):
203 return self.datetime_normalizer(value)
205 def serialize_wait(self, value):
206 return self.timestamp_serializer(value)
208 def deserialize_wait(self, value):
209 return self.timestamp_deserializer(value)
211 def normalize_wait(self, value):
212 return self.datetime_normalizer(value)
214 def serialize_annotations(self, value):
215 value = value if value is not None else []
217 # This may seem weird, but it's correct, we want to export
218 # a list of dicts as serialized value
219 serialized_annotations = [json.loads(annotation.export_data())
220 for annotation in value]
221 return serialized_annotations if serialized_annotations else ''
223 def deserialize_annotations(self, data):
224 return [TaskAnnotation(self, d) for d in data] if data else []
226 def serialize_tags(self, tags):
227 return ','.join(tags) if tags else ''
229 def deserialize_tags(self, tags):
230 if isinstance(tags, six.string_types):
231 return tags.split(',') if tags else []
234 def serialize_depends(self, value):
235 # Return the list of uuids
236 value = value if value is not None else set()
237 return ','.join(task['uuid'] for task in value)
239 def deserialize_depends(self, raw_uuids):
240 raw_uuids = raw_uuids or '' # Convert None to empty string
241 uuids = raw_uuids.split(',')
242 return set(self.warrior.tasks.get(uuid=uuid) for uuid in uuids if uuid)
244 def datetime_normalizer(self, value):
246 Normalizes date/datetime value (considered to come from user input)
247 to localized datetime value. Following conversions happen:
249 naive date -> localized datetime with the same date, and time=midnight
250 naive datetime -> localized datetime with the same value
251 localized datetime -> localized datetime (no conversion)
254 if (isinstance(value, datetime.date)
255 and not isinstance(value, datetime.datetime)):
256 # Convert to local midnight
257 value_full = datetime.datetime.combine(value, datetime.time.min)
258 localized = local_zone.localize(value_full)
259 elif isinstance(value, datetime.datetime):
260 if value.tzinfo is None:
261 # Convert to localized datetime object
262 localized = local_zone.localize(value)
264 # If the value is already localized, there is no need to change
265 # time zone at this point. Also None is a valid value too.
267 elif (isinstance(value, six.string_types)
268 and self.warrior.version >= VERSION_2_4_0):
269 # For strings, use 'task calc' to evaluate the string to datetime
270 # available since TW 2.4.0
272 result = self.warrior.execute_command(['calc'] + args)
273 naive = datetime.datetime.strptime(result[0], DATE_FORMAT_CALC)
274 localized = local_zone.localize(naive)
276 raise ValueError("Provided value could not be converted to "
277 "datetime, its type is not supported: {}"
278 .format(type(value)))
282 def normalize_uuid(self, value):
284 if not isinstance(value, six.string_types) or value == '':
285 raise ValueError("UUID must be a valid non-empty string, "
286 "not: {}".format(value))
291 class TaskResource(SerializingObject):
292 read_only_fields = []
294 def _load_data(self, data):
295 self._data = dict((key, self._deserialize(key, value))
296 for key, value in data.items())
297 # We need to use a copy for original data, so that changes
298 # are not propagated.
299 self._original_data = copy.deepcopy(self._data)
301 def _update_data(self, data, update_original=False, remove_missing=False):
303 Low level update of the internal _data dict. Data which are coming as
304 updates should already be serialized. If update_original is True, the
305 original_data dict is updated as well.
307 self._data.update(dict((key, self._deserialize(key, value))
308 for key, value in data.items()))
310 # In certain situations, we want to treat missing keys as removals
312 for key in set(self._data.keys()) - set(data.keys()):
313 self._data[key] = None
316 self._original_data = copy.deepcopy(self._data)
319 def __getitem__(self, key):
320 # This is a workaround to make TaskResource non-iterable
321 # over simple index-based iteration
328 if key not in self._data:
329 self._data[key] = self._deserialize(key, None)
331 return self._data.get(key)
333 def __setitem__(self, key, value):
334 if key in self.read_only_fields:
335 raise RuntimeError('Field \'%s\' is read-only' % key)
337 # Normalize the user input before saving it
338 value = self._normalize(key, value)
339 self._data[key] = value
342 s = six.text_type(self.__unicode__())
344 s = s.encode('utf-8')
350 def export_data(self):
352 Exports current data contained in the Task as JSON
355 # We need to remove spaces for TW-1504, use custom separators
356 data_tuples = ((key, self._serialize(key, value))
357 for key, value in six.iteritems(self._data))
359 # Empty string denotes empty serialized value, we do not want
360 # to pass that to TaskWarrior.
361 data_tuples = filter(lambda t: t[1] is not '', data_tuples)
362 data = dict(data_tuples)
363 return json.dumps(data, separators=(',',':'))
366 def _modified_fields(self):
367 writable_fields = set(self._data.keys()) - set(self.read_only_fields)
368 for key in writable_fields:
369 new_value = self._data.get(key)
370 old_value = self._original_data.get(key)
372 # Make sure not to mark data removal as modified field if the
373 # field originally had some empty value
374 if key in self._data and not new_value and not old_value:
377 if new_value != old_value:
382 return bool(list(self._modified_fields))
385 class TaskAnnotation(TaskResource):
386 read_only_fields = ['entry', 'description']
388 def __init__(self, task, data={}):
390 self._load_data(data)
391 super(TaskAnnotation, self).__init__(task.warrior)
394 self.task.remove_annotation(self)
396 def __unicode__(self):
397 return self['description']
399 def __eq__(self, other):
400 # consider 2 annotations equal if they belong to the same task, and
401 # their data dics are the same
402 return self.task == other.task and self._data == other._data
404 __repr__ = __unicode__
407 class Task(TaskResource):
408 read_only_fields = ['id', 'entry', 'urgency', 'uuid', 'modified']
410 class DoesNotExist(Exception):
413 class CompletedTask(Exception):
415 Raised when the operation cannot be performed on the completed task.
419 class DeletedTask(Exception):
421 Raised when the operation cannot be performed on the deleted task.
425 class ActiveTask(Exception):
427 Raised when the operation cannot be performed on the active task.
431 class InactiveTask(Exception):
433 Raised when the operation cannot be performed on an inactive task.
437 class NotSaved(Exception):
439 Raised when the operation cannot be performed on the task, because
440 it has not been saved to TaskWarrior yet.
445 def from_input(cls, input_file=sys.stdin, modify=None, warrior=None):
447 Creates a Task object, directly from the stdin, by reading one line.
448 If modify=True, two lines are used, first line interpreted as the
449 original state of the Task object, and second line as its new,
450 modified value. This is consistent with the TaskWarrior's hook
453 Object created by this method should not be saved, deleted
454 or refreshed, as t could create a infinite loop. For this
455 reason, TaskWarrior instance is set to None.
457 Input_file argument can be used to specify the input file,
458 but defaults to sys.stdin.
461 # Detect the hook type if not given directly
462 name = os.path.basename(sys.argv[0])
463 modify = name.startswith('on-modify') if modify is None else modify
465 # Create the TaskWarrior instance if none passed
467 hook_parent_dir = os.path.dirname(os.path.dirname(sys.argv[0]))
468 warrior = TaskWarrior(data_location=hook_parent_dir)
470 # TaskWarrior instance is set to None
473 # Load the data from the input
474 task._load_data(json.loads(input_file.readline().strip()))
476 # If this is a on-modify event, we are provided with additional
477 # line of input, which provides updated data
479 task._update_data(json.loads(input_file.readline().strip()),
484 def __init__(self, warrior, **kwargs):
485 super(Task, self).__init__(warrior)
487 # Check that user is not able to set read-only value in __init__
488 for key in kwargs.keys():
489 if key in self.read_only_fields:
490 raise RuntimeError('Field \'%s\' is read-only' % key)
492 # We serialize the data in kwargs so that users of the library
493 # do not have to pass different data formats via __setitem__ and
494 # __init__ methods, that would be confusing
496 # Rather unfortunate syntax due to python2.6 comaptiblity
497 self._data = dict((key, self._normalize(key, value))
498 for (key, value) in six.iteritems(kwargs))
499 self._original_data = copy.deepcopy(self._data)
501 # Provide read only access to the original data
502 self.original = ReadOnlyDictView(self._original_data)
504 def __unicode__(self):
505 return self['description']
507 def __eq__(self, other):
508 if self['uuid'] and other['uuid']:
509 # For saved Tasks, just define equality by equality of uuids
510 return self['uuid'] == other['uuid']
512 # If the tasks are not saved, compare the actual instances
513 return id(self) == id(other)
518 # For saved Tasks, just define equality by equality of uuids
519 return self['uuid'].__hash__()
521 # If the tasks are not saved, return hash of instance id
522 return id(self).__hash__()
526 return self['status'] == six.text_type('completed')
530 return self['status'] == six.text_type('deleted')
534 return self['status'] == six.text_type('waiting')
538 return self['status'] == six.text_type('pending')
542 return self['start'] is not None
546 return self['uuid'] is not None or self['id'] is not None
548 def serialize_depends(self, cur_dependencies):
549 # Check that all the tasks are saved
550 for task in (cur_dependencies or set()):
552 raise Task.NotSaved('Task \'%s\' needs to be saved before '
553 'it can be set as dependency.' % task)
555 return super(Task, self).serialize_depends(cur_dependencies)
557 def format_depends(self):
558 # We need to generate added and removed dependencies list,
559 # since Taskwarrior does not accept redefining dependencies.
561 # This cannot be part of serialize_depends, since we need
562 # to keep a list of all depedencies in the _data dictionary,
563 # not just currently added/removed ones
565 old_dependencies = self._original_data.get('depends', set())
567 added = self['depends'] - old_dependencies
568 removed = old_dependencies - self['depends']
570 # Removed dependencies need to be prefixed with '-'
571 return 'depends:' + ','.join(
572 [t['uuid'] for t in added] +
573 ['-' + t['uuid'] for t in removed]
576 def format_description(self):
577 # Task version older than 2.4.0 ignores first word of the
578 # task description if description: prefix is used
579 if self.warrior.version < VERSION_2_4_0:
580 return self._data['description']
582 return six.u("description:'{0}'").format(self._data['description'] or '')
586 raise Task.NotSaved("Task needs to be saved before it can be deleted")
588 # Refresh the status, and raise exception if the task is deleted
589 self.refresh(only_fields=['status'])
592 raise Task.DeletedTask("Task was already deleted")
594 self.warrior.execute_command([self['uuid'], 'delete'])
596 # Refresh the status again, so that we have updated info stored
597 self.refresh(only_fields=['status', 'start', 'end'])
601 raise Task.NotSaved("Task needs to be saved before it can be started")
603 # Refresh, and raise exception if task is already completed/deleted
604 self.refresh(only_fields=['status'])
607 raise Task.CompletedTask("Cannot start a completed task")
609 raise Task.DeletedTask("Deleted task cannot be started")
611 raise Task.ActiveTask("Task is already active")
613 self.warrior.execute_command([self['uuid'], 'start'])
615 # Refresh the status again, so that we have updated info stored
616 self.refresh(only_fields=['status', 'start'])
620 raise Task.NotSaved("Task needs to be saved before it can be stopped")
622 # Refresh, and raise exception if task is already completed/deleted
623 self.refresh(only_fields=['status'])
626 raise Task.InactiveTask("Cannot stop an inactive task")
628 self.warrior.execute_command([self['uuid'], 'stop'])
630 # Refresh the status again, so that we have updated info stored
631 self.refresh(only_fields=['status', 'start'])
635 raise Task.NotSaved("Task needs to be saved before it can be completed")
637 # Refresh, and raise exception if task is already completed/deleted
638 self.refresh(only_fields=['status'])
641 raise Task.CompletedTask("Cannot complete a completed task")
643 raise Task.DeletedTask("Deleted task cannot be completed")
645 # Older versions of TW do not stop active task at completion
646 if self.warrior.version < VERSION_2_4_0 and self.active:
649 self.warrior.execute_command([self['uuid'], 'done'])
651 # Refresh the status again, so that we have updated info stored
652 self.refresh(only_fields=['status', 'start', 'end'])
655 if self.saved and not self.modified:
658 args = [self['uuid'], 'modify'] if self.saved else ['add']
659 args.extend(self._get_modified_fields_as_args())
660 output = self.warrior.execute_command(args)
662 # Parse out the new ID, if the task is being added for the first time
664 id_lines = [l for l in output if l.startswith('Created task ')]
666 # Complain loudly if it seems that more tasks were created
668 if len(id_lines) != 1 or len(id_lines[0].split(' ')) != 3:
669 raise TaskWarriorException("Unexpected output when creating "
670 "task: %s" % '\n'.join(id_lines))
672 # Circumvent the ID storage, since ID is considered read-only
673 self._data['id'] = int(id_lines[0].split(' ')[2].rstrip('.'))
675 # Refreshing is very important here, as not only modification time
676 # is updated, but arbitrary attribute may have changed due hooks
677 # altering the data before saving
680 def add_annotation(self, annotation):
682 raise Task.NotSaved("Task needs to be saved to add annotation")
684 args = [self['uuid'], 'annotate', annotation]
685 self.warrior.execute_command(args)
686 self.refresh(only_fields=['annotations'])
688 def remove_annotation(self, annotation):
690 raise Task.NotSaved("Task needs to be saved to remove annotation")
692 if isinstance(annotation, TaskAnnotation):
693 annotation = annotation['description']
694 args = [self['uuid'], 'denotate', annotation]
695 self.warrior.execute_command(args)
696 self.refresh(only_fields=['annotations'])
698 def _get_modified_fields_as_args(self):
701 def add_field(field):
702 # Add the output of format_field method to args list (defaults to
704 serialized_value = self._serialize(field, self._data[field])
706 # Empty values should not be enclosed in quotation marks, see
708 if serialized_value is '':
709 escaped_serialized_value = ''
711 escaped_serialized_value = six.u("'{0}'").format(serialized_value)
713 format_default = lambda: six.u("{0}:{1}").format(field,
714 escaped_serialized_value)
716 format_func = getattr(self, 'format_{0}'.format(field),
719 args.append(format_func())
721 # If we're modifying saved task, simply pass on all modified fields
723 for field in self._modified_fields:
725 # For new tasks, pass all fields that make sense
727 for field in self._data.keys():
728 if field in self.read_only_fields:
734 def refresh(self, only_fields=[]):
735 # Raise error when trying to refresh a task that has not been saved
737 raise Task.NotSaved("Task needs to be saved to be refreshed")
739 # We need to use ID as backup for uuid here for the refreshes
740 # of newly saved tasks. Any other place in the code is fine
741 # with using UUID only.
742 args = [self['uuid'] or self['id'], 'export']
743 new_data = json.loads(self.warrior.execute_command(args)[0])
746 [(k, new_data.get(k)) for k in only_fields])
747 self._update_data(to_update, update_original=True)
749 self._load_data(new_data)
751 class TaskFilter(SerializingObject):
753 A set of parameters to filter the task list with.
756 def __init__(self, warrior, filter_params=[]):
757 self.filter_params = filter_params
758 super(TaskFilter, self).__init__(warrior)
760 def add_filter(self, filter_str):
761 self.filter_params.append(filter_str)
763 def add_filter_param(self, key, value):
764 key = key.replace('__', '.')
766 # Replace the value with empty string, since that is the
767 # convention in TW for empty values
768 attribute_key = key.split('.')[0]
770 # Since this is user input, we need to normalize before we serialize
771 value = self._normalize(attribute_key, value)
772 value = self._serialize(attribute_key, value)
774 # If we are filtering by uuid:, do not use uuid keyword
777 self.filter_params.insert(0, value)
779 # Surround value with aphostrophes unless it's a empty string
780 value = "'%s'" % value if value else ''
782 # We enforce equality match by using 'is' (or 'none') modifier
783 # Without using this syntax, filter fails due to TW-1479
784 modifier = '.is' if value else '.none'
785 key = key + modifier if '.' not in key else key
787 self.filter_params.append(six.u("{0}:{1}").format(key, value))
789 def get_filter_params(self):
790 return [f for f in self.filter_params if f]
793 c = self.__class__(self.warrior)
794 c.filter_params = list(self.filter_params)
798 class TaskQuerySet(object):
800 Represents a lazy lookup for a task objects.
803 def __init__(self, warrior=None, filter_obj=None):
804 self.warrior = warrior
805 self._result_cache = None
806 self.filter_obj = filter_obj or TaskFilter(warrior)
808 def __deepcopy__(self, memo):
810 Deep copy of a QuerySet doesn't populate the cache
812 obj = self.__class__()
813 for k, v in self.__dict__.items():
814 if k in ('_iter', '_result_cache'):
815 obj.__dict__[k] = None
817 obj.__dict__[k] = copy.deepcopy(v, memo)
821 data = list(self[:REPR_OUTPUT_SIZE + 1])
822 if len(data) > REPR_OUTPUT_SIZE:
823 data[-1] = "...(remaining elements truncated)..."
827 if self._result_cache is None:
828 self._result_cache = list(self)
829 return len(self._result_cache)
832 if self._result_cache is None:
833 self._result_cache = self._execute()
834 return iter(self._result_cache)
836 def __getitem__(self, k):
837 if self._result_cache is None:
838 self._result_cache = list(self)
839 return self._result_cache.__getitem__(k)
842 if self._result_cache is not None:
843 return bool(self._result_cache)
846 except StopIteration:
850 def __nonzero__(self):
851 return type(self).__bool__(self)
853 def _clone(self, klass=None, **kwargs):
855 klass = self.__class__
856 filter_obj = self.filter_obj.clone()
857 c = klass(warrior=self.warrior, filter_obj=filter_obj)
858 c.__dict__.update(kwargs)
863 Fetch the tasks which match the current filters.
865 return self.warrior.filter_tasks(self.filter_obj)
869 Returns a new TaskQuerySet that is a copy of the current one.
874 return self.filter(status=PENDING)
877 return self.filter(status=COMPLETED)
879 def filter(self, *args, **kwargs):
881 Returns a new TaskQuerySet with the given filters added.
883 clone = self._clone()
885 clone.filter_obj.add_filter(f)
886 for key, value in kwargs.items():
887 clone.filter_obj.add_filter_param(key, value)
890 def get(self, **kwargs):
892 Performs the query and returns a single object matching the given
895 clone = self.filter(**kwargs)
898 return clone._result_cache[0]
900 raise Task.DoesNotExist(
901 'Task matching query does not exist. '
902 'Lookup parameters were {0}'.format(kwargs))
904 'get() returned more than one Task -- it returned {0}! '
905 'Lookup parameters were {1}'.format(num, kwargs))
908 class TaskWarrior(object):
909 def __init__(self, data_location=None, create=True, taskrc_location='~/.taskrc'):
910 self.taskrc_location = os.path.expanduser(taskrc_location)
912 # If taskrc does not exist, pass / to use defaults and avoid creating
913 # dummy .taskrc file by TaskWarrior
914 if not os.path.exists(self.taskrc_location):
915 self.taskrc_location = '/'
917 self.version = self._get_version()
919 'confirmation': 'no',
920 'dependency.confirmation': 'no', # See TW-1483 or taskrc man page
921 'recurrence.confirmation': 'no', # Necessary for modifying R tasks
923 # Defaults to on since 2.4.5, we expect off during parsing
926 # 2.4.3 onwards supports 0 as infite bulk, otherwise set just
927 # arbitrary big number which is likely to be large enough
928 'bulk': 0 if self.version >= VERSION_2_4_3 else 100000,
931 # Set data.location override if passed via kwarg
932 if data_location is not None:
933 data_location = os.path.expanduser(data_location)
934 if create and not os.path.exists(data_location):
935 os.makedirs(data_location)
936 self.config['data.location'] = data_location
938 self.tasks = TaskQuerySet(self)
940 def _get_command_args(self, args, config_override={}):
941 command_args = ['task', 'rc:{0}'.format(self.taskrc_location)]
942 config = self.config.copy()
943 config.update(config_override)
944 for item in config.items():
945 command_args.append('rc.{0}={1}'.format(*item))
946 command_args.extend(map(six.text_type, args))
949 def _get_version(self):
950 p = subprocess.Popen(
951 ['task', '--version'],
952 stdout=subprocess.PIPE,
953 stderr=subprocess.PIPE)
954 stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
955 return stdout.strip('\n')
957 def get_config(self):
958 raw_output = self.execute_command(
960 config_override={'verbose': 'nothing'}
964 config_regex = re.compile(r'^(?P<key>[^\s]+)\s+(?P<value>[^\s].+$)')
966 for line in raw_output:
967 match = config_regex.match(line)
969 config[match.group('key')] = match.group('value').strip()
973 def execute_command(self, args, config_override={}, allow_failure=True,
975 command_args = self._get_command_args(
976 args, config_override=config_override)
977 logger.debug(' '.join(command_args))
978 p = subprocess.Popen(command_args, stdout=subprocess.PIPE,
979 stderr=subprocess.PIPE)
980 stdout, stderr = [x.decode('utf-8') for x in p.communicate()]
981 if p.returncode and allow_failure:
983 error_msg = stderr.strip()
985 error_msg = stdout.strip()
986 raise TaskWarriorException(error_msg)
988 # Return all whole triplet only if explicitly asked for
990 return stdout.rstrip().split('\n')
992 return (stdout.rstrip().split('\n'),
993 stderr.rstrip().split('\n'),
996 def enforce_recurrence(self):
997 # Run arbitrary report command which will trigger generation
998 # of recurrent tasks.
1000 # Only necessary for TW up to 2.4.1, fixed in 2.4.2.
1001 if self.version < VERSION_2_4_2:
1002 self.execute_command(['next'], allow_failure=False)
1004 def filter_tasks(self, filter_obj):
1005 self.enforce_recurrence()
1006 args = ['export', '--'] + filter_obj.get_filter_params()
1008 for line in self.execute_command(args):
1010 data = line.strip(',')
1012 filtered_task = Task(self)
1013 filtered_task._load_data(json.loads(data))
1014 tasks.append(filtered_task)
1016 raise TaskWarriorException('Invalid JSON: %s' % data)
1019 def merge_with(self, path, push=False):
1020 path = path.rstrip('/') + '/'
1021 self.execute_command(['merge', path], config_override={
1022 'merge.autopush': 'yes' if push else 'no',
1026 self.execute_command(['undo'])