X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/fe6b70b78cf3fbd333fe6ee2bbb5c4cb55dacf0b..dcf2b8b93cbfc94a86b64d03c0c154a6db671b44:/tasklib/backends.py diff --git a/tasklib/backends.py b/tasklib/backends.py index fbe3ba3..38f6c59 100644 --- a/tasklib/backends.py +++ b/tasklib/backends.py @@ -7,9 +7,8 @@ import os import re import six import subprocess -import copy -from .task import Task, TaskQuerySet +from .task import Task, TaskQuerySet, ReadOnlyDictView from .filters import TaskWarriorFilter from .serializing import local_zone @@ -76,7 +75,7 @@ class Backend(object): Converts TW syntax datetime string to a localized datetime object. This method is not mandatory. """ - raise NotImplemented + raise NotImplementedError class TaskWarriorException(Exception): @@ -95,17 +94,23 @@ class TaskWarrior(Backend): 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='~/.taskrc'): - self.taskrc_location = os.path.expanduser(taskrc_location) + 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 = '/' - # 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 = self._get_version() - self.config = { + 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 @@ -123,22 +128,28 @@ class TaskWarrior(Backend): data_location = os.path.expanduser(data_location) if create and not os.path.exists(data_location): os.makedirs(data_location) - self.config['data.location'] = 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 = ['task', 'rc:{0}'.format(self.taskrc_location)] - config = self.config.copy() - config.update(config_override or dict()) - for item in config.items(): + 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(map(six.text_type, args)) + 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( - ['task', '--version'], + self._get_task_command() + ['--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = [x.decode('utf-8') for x in p.communicate()] @@ -172,11 +183,17 @@ class TaskWarrior(Backend): 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 @@ -206,21 +223,25 @@ class TaskWarrior(Backend): if self.version < self.VERSION_2_4_0: return task._data['description'] else: - return six.u("description:'{0}'").format(task._data['description'] or '') + 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 'task calc' to evaluate the string to datetime + # 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))) + raise ValueError( + 'Provided value could not be converted to ' + 'datetime, its type is not supported: {}' + .format(type(value)), + ) return localized @@ -234,7 +255,7 @@ class TaskWarrior(Backend): def config(self): # First, check if memoized information is available if self._config: - return copy.deepcopy(self._config) + return self._config # If not, fetch the config using the 'show' command raw_output = self.execute_command( @@ -243,7 +264,7 @@ class TaskWarrior(Backend): ) config = dict() - config_regex = re.compile(r'^(?P[^\s]+)\s+(?P[^\s].+$)') + config_regex = re.compile(r'^(?P[^\s]+)\s+(?P[^\s].*$)') for line in raw_output: match = config_regex.match(line) @@ -251,23 +272,28 @@ class TaskWarrior(Backend): config[match.group('key')] = match.group('value').strip() # Memoize the config dict - self._config = config + self._config = ReadOnlyDictView(config) - return copy.deepcopy(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(' '.join(command_args)) + 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) + 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 @@ -299,7 +325,7 @@ class TaskWarrior(Backend): def filter_tasks(self, filter_obj): self.enforce_recurrence() - args = ['export', '--'] + filter_obj.get_filter_params() + args = ['export'] + filter_obj.get_filter_params() tasks = [] for line in self.execute_command(args): if line: @@ -324,10 +350,14 @@ class TaskWarrior(Backend): 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 - if len(id_lines) != 1 or len(id_lines[0].split(' ')) != 3: - raise TaskWarriorException("Unexpected output when creating " - "task: %s" % '\n'.join(id_lines)) + # 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('.') @@ -393,14 +423,14 @@ class TaskWarrior(Backend): for key, value in data.items(): taskfilter.add_filter_param(key, value) - output = self.execute_command(['export', '--'] + + 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( + 'Unique identifiers {0} with description: {1} matches ' + 'multiple tasks: {2}'.format( task['uuid'] or task['id'], task['description'], output) )