nutools/lib/pyulib/migrate/tasks2/Tasks.py

1712 lines
60 KiB
Python
Raw Normal View History

2013-08-27 15:14:44 +04:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
u"""%(scriptname)s: gérer une liste de tâches
USAGE
%(scriptname)s [options] list|add|edit|do|commit|remove|purge|report|update [args]
OPTIONS
-c tasksrc
Spécifier le fichier de configuration
Utilisez l'option --help avec chacune des commandes pour avoir une
description plus complète de leur utilisation"""
__all__ = ('Task', 'TextFile', 'TWFile', 'TasksrcFile', 'TasksCLI')
import sys, os, re
from os import path
from getopt import getopt
from types import StringType, UnicodeType, ListType, TupleType
from pyutools.dates import datef
from pyutools.config import ShConfigFile
from pyutools import scriptdir, scriptname, dict, ensure_unicode
from pyutools import uprint, get_color, eerror, ewarn, enote
from pyutools import edit_template
from TiddlyWiki import Tiddler, TiddlyWiki
_marker = []
DEFAULT_ENCODING = 'iso-8859-1'
# propriétés standard d'une tâche
ID = 'id'
TITLE = 'title'
DESC = 'desc'
TEXT = 'text'
ENCODING = 'encoding'
DATE = 'date' # date de création
TYPE = 'type' # type de tâche. par défaut, vaut 'TODO'
# propriétés pour une tâche de type TODO
TODO_TYPE = 'TODO'
DONE = 'done' # date de fin de tâche
RELEASED_SUFFIX = '_released' # date de release du projet
PURGED_SUFFIX = '_purged' # date de purge du fichier pour le projet
# propriétés pour une tâche de type IDEA
IDEA_TYPE = 'IDEA'
class Task:
# ligne telle qu'elle est écrite dans le fichier
line = None
# le texte, les propriétés, les projets, les contextes
text = None
props = None
projects = None
contexts = None
# cet objet a-t-il été modifié?
modified = False
# numero d'index de cet tâche dans la liste des tâches
index = None
encoding = DEFAULT_ENCODING
def ensure_unicode(self, value):
return ensure_unicode(value, self.encoding)
def quote_nl(self, text):
if text is not None:
text = text.replace('\\', r'\\')
text = text.replace('\n', r'\n')
return text
def unquote_nl(self, text):
if text is not None:
text = text.replace(r'\n', '\n')
text = text.replace(r'\\', '\\')
return text
##################################################################
def __init__(self, line=None, **kw):
self.text = u''
self.props = {}
self.projects = []
self.contexts = []
if line is not None: self.fromline(line)
for name, value in kw.items():
self[name] = value
TAGS_PATTERN = re.compile(r'\s*\{\s*((?:(?:\+\S+\s*)?(?:@\S+\s*)?(?:\S+=\S+\s*)?)*)\}\s*$')
PROP_PATTERN = re.compile(r'(\S+)=(\S+)')
PROJ_PATTERN = re.compile(r'\+(\S+)\s*')
CTXT_PATTERN = re.compile(r'@(\S+)\s*')
def fromline(self, line):
"""Initialiser cet objet à partir d'une ligne d'un fichier.
"""
mo = self.TAGS_PATTERN.search(line)
if mo is not None:
tags = mo.group(1)
props = self.PROP_PATTERN.findall(tags)
for name, value in props:
if name == ENCODING: self[name] = value
for name, value in props:
if name != ENCODING: self[name] = value
self.projects = []
for project in self.PROJ_PATTERN.findall(tags):
self.projects.append(self.ensure_unicode(project))
self.contexts = []
for context in self.CTXT_PATTERN.findall(tags):
self.contexts.append(self.ensure_unicode(context))
self.text = self.ensure_unicode(self.unquote_nl(line[:mo.start(0)]))
else:
self.text = self.ensure_unicode(self.unquote_nl(line))
def toline(self):
"""Générer une ligne d'un fichier à partir de cet objet.
"""
if self.line is None:
line = self.quote_nl(self.text or u'')
if self.props:
prefix = u' {'
for project in self.projects:
line += u'%s+%s' % (prefix, project)
prefix = u' '
for context in self.contexts:
line += u'%s@%s' % (prefix, context)
prefix = u' '
for name, value in self.props.items():
line += u'%s%s=%s' % (prefix, name, value)
prefix = u' '
line += u'}'
self.line = line.encode(self.encoding)
return self.line
__repr__ = toline
__str__ = toline
def update_from(self, t):
for n in ('line', 'text', 'props', 'projects', 'contexts'):
tp = getattr(self, n)
op = getattr(t, n)
if tp != op:
setattr(self, n, op)
self.modified = True
# Gestion des propriétés
def __eq__(self, other):
return self is other
def __len__(self):
return len(self.props)
def __getattr__(self, name):
if name in self: return self[name]
else: raise NameError, name
def __getitem__(self, name, default=_marker):
if name in self.METH_PROPERTIES:
return getattr(self, 'get_' + name)()
else:
if default is _marker: return self.props[name]
else: return self.props.get(name, default)
get = __getitem__
def __delitem__(self, name):
if name in self.METH_PROPERTIES: getattr(self, 'del_' + name)()
else: del self.props[name]
self.line = None
self.modified = True
def __setitem__(self, name, value):
if name in self.METH_PROPERTIES: getattr(self, 'set_' + name)(value)
else: self.props[name] = self.ensure_unicode(value)
self.line = None
self.modified = True
def is_modified(self):
return self.modified
def __contains__(self, key):
return key in self.METH_PROPERTIES or key in self.props
def has_key(self, key):
return key in self.METH_PROPERTIES or self.props.has_key(key)
def keys(self):
return self.props.keys()
def values(self):
return self.props.values()
def items(self):
return self.props.items()
def clear(self):
self.props.clear()
def update(self, props=None, **kw):
if props is not None:
for key, value in props.items():
self[key] = value
for key, value in kw.items():
self[key] = value
# Propriétés gérées par des méthodes
METH_PROPERTIES = [TITLE, DESC, TEXT, ENCODING]
def get_title(self):
pos = self.text.find('\n\n')
if pos != -1: return self.text[:pos]
else: return self.text
def del_title(self):
self.set_title('')
def set_title(self, title):
title = self.ensure_unicode(title)
desc = self.get_desc()
self.text = title + (desc and u'\n\n' + desc or '')
self.line = None
self.modified = True
def get_desc(self):
text = self.text or u''
pos = text.find('\n\n')
if pos != -1: return text[pos + 2:]
else: return None
def del_desc(self):
self.set_desc('')
def set_desc(self, desc):
title = self.get_title()
desc = self.ensure_unicode(desc)
self.text = title + (desc and u'\n\n' + desc or '')
self.line = None
self.modified = True
def get_text(self):
return self.text
def del_text(self):
self.set_text('')
def set_text(self, text):
self.text = self.ensure_unicode(text)
self.line = None
self.modified = True
def get_encoding(self):
return self.encoding
def del_encoding(self):
self.encoding = DEFAULT_ENCODING
del self.props[ENCODING]
self.modified = True
def set_encoding(self, encoding):
self.encoding = encoding
self.props[ENCODING] = encoding
self.modified = True
# Opérations de haut niveau
def has_id(self):
return self.has_key(ID)
def get_id(self):
return self.get(ID, None)
def set_id(self, id=None):
if id is None:
if self.has_key(ID): del self[ID]
else:
self[ID] = id
def has_date(self):
return self.has_key(DATE)
def get_date(self):
return self.get(DATE, None)
def set_date(self, date=None):
if date is None: date = datef()
self[DATE] = date
def get_type(self):
return self.get(TYPE, TODO_TYPE)
def set_type(self, type=None):
if type is not None:
type = type.upper()
if type in ('T', 'TODO'): type = TODO_TYPE
elif type in ('I', 'IDEA'): type = IDEA_TYPE
if type is None or type == TODO_TYPE:
if self.has_key(TYPE): del self[TYPE]
else:
self[TYPE] = type
def is_todo_type(self):
return self.get_type() == TODO_TYPE
def is_done(self):
return self.has_key(DONE)
def set_done(self, date=None):
if date is None: date = datef()
self[DONE] = date
def is_idea_type(self):
return self.get_type() == IDEA_TYPE
def has_projects(self):
return len(self.projects) != 0
def get_projects(self):
return self.projects
def set_projects(self, projects):
self.projects = list(projects)
self.modified = True
def has_project(self, project):
return project in self.projects
def add_project(self, project):
self.projects.append(project)
def remove_project(self, project):
self.projects.remove(project)
def is_project_purged(self, project):
return self.has_key(project + PURGED_SUFFIX)
def set_project_purged(self, project, date=None):
if date is None: date = datef()
self[project + PURGED_SUFFIX] = date
def is_all_purged(self, is_project_func=None):
for project in self.projects:
if is_project_func is not None:
if not is_project_func(project): continue
if not self.is_project_purged(project): return False
return True
def is_project_released(self, project):
return self.has_key(project + RELEASED_SUFFIX)
def set_project_released(self, project, date=None):
if date is None: date = datef()
self[project + RELEASED_SUFFIX] = date
def is_all_released(self):
for project in self.projects:
if not self.is_project_released(project): return False
return True
def has_contexts(self):
return len(self.contexts) != 0
def get_contexts(self):
return self.contexts
def set_contexts(self, contexts):
self.contexts = list(contexts)
self.modified = True
def add_context(self, context):
self.contexts.append(context)
def remove_context(self, context):
self.contexts.remove(context)
class TasksFile:
"""Classe de base des fichiers contenant des tâches.
"""
valid = False
tasks = None
props = None
def update_indexes(self):
index = 1
for task in self.tasks:
task.index = index
index = index + 1
def update_ids(self):
known_ids = {}
for task in self.tasks:
if task.has_id(): known_ids[task.get_id()] = None
lastid = self.get_lastid()
for task in self.tasks:
if not task.has_key(ID):
id = lastid + 1
if known_ids.has_key(id):
while known_ids.has_key(id):
id = id + 1
task[ID] = id
lastid = id
self.set_lastid(lastid)
def load(self, file, raise_exception=True):
pass
def save(self, file=None, raise_exception=True):
pass
def is_valid(self):
return self.valid
def get_file(self):
pass
def get_tasks(self):
return self.tasks
def __len__(self):
return len(self.tasks)
def __getitem__(self, index):
return self.tasks[index]
def __delitem__(self, index):
del self.tasks[index]
self.update_indexes()
def __setitem__(self, index, task):
self.tasks[index] = task
self.update_ids()
def add(self, task):
self.tasks.append(task)
self.update_indexes()
self.update_ids()
def remove(self, task_or_id):
if isinstance(task_or_id, Task): task = task_or_id
else: task = self.get_by_id(task_or_id)
self.tasks.remove(task)
self.update_indexes()
self.update_ids()
def __filter_indexes(self, tasks, indexes):
if not indexes: return tasks
filtered = []
maxindex = max(map(lambda t: t.index, tasks))
for i in indexes:
if i > 0 and i <= maxindex:
filtered.append(tasks[i - 1])
return filtered
def __filter_contexts(self, tasks, contexts):
if not contexts: return tasks
if '*' in contexts: return tasks
filtered = []
for t in tasks:
for c in contexts:
if not t.has_context(c): break
else:
filtered.append(t)
return filtered
def __filter_projects(self, tasks, projects):
if not projects: return tasks
if '*' in projects: return tasks
filtered = []
for t in tasks:
for p in projects:
if not t.has_project(p): break
else:
filtered.append(t)
return filtered
def __filter_patterns(self, tasks, patterns):
if not patterns: return tasks
filtered = []
for t in tasks:
for p in patterns:
if not re.search(p, t.get_text()): break
else:
filtered.append(t)
return filtered
def __filter_props(self, tasks, props):
if not props: return tasks
filtered = []
for t in tasks:
for name, value in props.items():
if value == '*':
if not t.has_key(name): break
elif value == '':
if t.has_key(name): break
else:
if not t.has_key(name) or t[name] != value: break
else:
filtered.append(t)
return filtered
def filter(self, indexes=None, projects=None, contexts=None, patterns=None, props=None, **kw):
filtered = self.__filter_indexes(self.get_tasks(), indexes)
filtered = self.__filter_contexts(filtered, contexts)
filtered = self.__filter_projects(filtered, projects)
filtered = self.__filter_patterns(filtered, patterns)
if props is None: props = kw
else: props.update(kw)
filtered = self.__filter_props(filtered, props)
return filtered
def get_by_id(self, id):
filtered = self.filter(props={ID: id})
if len(filtered) > 0: return filtered[0]
else: return None
def has_props(self):
return len(self.props) != 0
def has_prop(self, name):
return self.props.has_key(name)
def get_prop(self, name, default=_marker):
value = self.props.get(name, default)
if value is _marker: value = None
return value
def set_prop(self, name, value):
self.props[name] = value
# Propriétés standard d'un fichier texte
TF_LASTID = 'lastid'
TF_ENCODING = 'encoding' # encoding par défaut du fichier
class TextFile(TasksFile):
"""Fichier de tâches au format texte.
"""
TASKS_TXT = ['TASKS.txt', 'tasks.txt']
PURGED_TXT = ['PURGED.txt', 'purged.txt']
FILENAMES = [TASKS_TXT, PURGED_TXT]
TASKS_FILETYPE = 0
PURGED_FILETYPE = 1
valid = False
file = pf = dirname = filename = None
tasks = None
props = None
def __init__(self, src=None, filetype=TASKS_FILETYPE, raise_exception=False):
self.tasks = []
self.props = {}
filenames = self.FILENAMES[filetype]
if src is None:
# Mettre en place les éléments pour qu'une sauvegarde écrive dans le fichier
# du type spécifié.
file = filenames[0]
self.__update_file(file)
self.valid = True
else:
# Sinon on essaye de charger le fichier
if path.isdir(src):
dir = src
file = path.join(dir, filenames[0])
if not path.exists(file):
file2 = path.join(dir, filenames[1])
if path.exists(file2): file = file2
else:
file = src
self.load(file, raise_exception=raise_exception)
def __update_file(self, file):
self.file = file
self.pf = path.abspath(self.file)
self.dirname, self.filename = path.split(self.pf)
def __strip_nl(self, line):
if line.endswith('\r\n'): return line[:-2]
elif line.endswith('\n'): return line[:-1]
elif line.endswith('\r'): return line[:-1]
else: return line
def __add_nl(self, line):
return line + '\n'
PROP_PATTERN = re.compile(r'(\S+)=(\S+)')
def __load_props(self, line):
for name, value in self.PROP_PATTERN.findall(line):
self.set_prop(name, value)
def load(self, file, raise_exception=True):
self.__update_file(file)
try:
self.tasks = []
self.valid = False
inf = open(self.pf, 'rb')
try:
lines = map(self.__strip_nl, inf.readlines())
self.valid = True
finally:
inf.close()
except IOError:
if raise_exception: raise
return False
if lines[0:1] and lines[0].startswith("##"):
self.__load_props(lines[0])
lines = lines[1:]
for line in lines:
t = Task(line)
t.modified = False
self.tasks.append(t)
self.update_indexes()
self.update_ids()
return True
def save(self, file=None, raise_exception=True):
if file is not None: self.__update_file(file)
self.valid = False
try:
# écrire dans un ficher temporaire d'abord
tmpf = self.pf + '.tmp'
outf = open(tmpf, 'wb')
try:
lines = []
if self.has_props():
line = "##"
for name, value in self.props.items():
line += " %s=%s" % (name, value)
lines.append(line)
lines.extend(map(lambda t: t.toline(), self.tasks))
outf.writelines(map(self.__add_nl, lines))
finally:
outf.close()
try:
os.rename(tmpf, self.pf)
except OSError:
os.remove(self.pf)
os.rename(tmpf, self.pf)
self.valid = True
return True
except IOError:
if raise_exception: raise
return False
def get_file(self):
return self.pf
def get_lastid(self):
return int(self.get_prop(TF_LASTID, 0))
def set_lastid(self, lastid):
self.set_prop(TF_LASTID, int(lastid))
class TWFile(TasksFile):
"""Fichier de tâches au format TiddlyWiki
"""
valid = False
file = pf = dirname = filename = None
twfile = None
tasks = None
props = None
def __init__(self, src=None, raise_exception=False):
self.tasks = []
self.props = {}
if src is None:
twfile = TiddlyWiki(src, raise_exception=False)
if twfile.is_valid():
self.load(twfile=twfile)
elif raise_exception:
raise IOError("Impossible de charger le TiddlyWiki associé")
else:
self.load(src, raise_exception=raise_exception)
def __update_file(self, file):
self.file = file
self.pf = path.abspath(self.file)
self.dirname, self.filename = path.split(self.pf)
def load(self, file, raise_exception=True):
self.valid = False
# XXX continuer ici
twfile = TiddlyWiki(src, raise_exception=False)
def save(self, file=None, raise_exception=True):
pass
class ProxyTask:
__task = None
index = None
def __init__(self, task, index):
self.__task = task
self.index = index
def __getattr__(self, name, default=_marker):
if default is _marker: return getattr(self.__task, name)
else: return getattr(self.__task, name, default)
class FileAggregate(TasksFile):
__tfs = None
def __init__(self, tfs):
self.__tfs = []
for tf in tfs:
self.__tfs.append(tf)
def load(self, file, raise_exception=True):
raise NotImplementedError
def save(self, file=None, raise_exception=True):
for tf in self.__tfs:
tf.save(file, raise_exception)
def __len__(self):
return reduce(lambda n, m: n + m, map(lambda t: len(t), self.__tfs))
def __get_tfindex(self, index):
tfi = index
for tf in self.__tfs:
if tfi < len(tf): return tf, tfi
tfi -= len(tf)
raise IndexError, index
def __getitem__(self, index):
tf, tfi = self.__get_tfindex(index)
return ProxyTask(tf[tfi], index)
def __delitem__(self, index):
tf, tfi = self.__get_tfindex(index)
del tf[tfi]
def __setitem__(self, index, task):
tf, tfi = self.__get_tfindex(index)
tf[tfi] = task
def add(self, task):
raise NotImplementedError
def remove(self, task):
raise NotImplementedError
def filter(self, indexes=None, projects=None, contexts=None, props=None, **kw):
offset = 0
filtered = []
for tf in self.__tfs:
for t in tf.filter(indexes, projects, contexts, props, **kw):
filtered.append(ProxyTask(t, t.index + offset))
offset += len(tf)
return filtered
def has_props(self):
for tf in self.__tfs:
if tf.has_props(): return True
return False
def has_prop(self, name):
for tf in self.__tfs:
if tf.has_prop(name): return True
return False
def get_prop(self, name, default=_marker):
for tf in self.__tfs:
if tf.has_prop(name): return tf.get_prop(name)
if default is _marker: return None
else: return default
def set_prop(self, name, value):
raise NotImplementedError
def get_lastid(self):
raise NotImplementedError
def set_lastid(self, lastid):
raise NotImplementedError
def is_valid(self):
raise NotImplementedError
def get_file(self):
raise NotImplementedError
######################################################################
class TasksrcFile(ShConfigFile):
TASKSRC = "tasksrc"
def __init__(self, file=None, raise_exception=True):
if file is None:
testdir = path.join(scriptdir, "test")
utoolsrcdir = path.join(path.expanduser("~"), ".utools")
if path.isdir(testdir): file = path.join(testdir, self.TASKSRC)
else: file = path.join(utoolsrcdir, self.TASKSRC)
raise_exception = False
ShConfigFile.__init__(self, file=file, raise_exception=raise_exception)
TASKSDIR = "tasksdir"
PROJDIRS = "projdirs"
TASKSFILE = "tasksfile"
PURGEDFILE = "purgedfile"
DONE_COLOR = "done_color"
IDEA_COLOR = "idea_color"
PROJECT_COLOR = "project_color"
CONTEXT_COLOR = "context_color"
TAG_COLOR = "tag_color"
def __p(self, p, refdir=None):
if refdir is not None:
if not path.isdir(refdir): refdir = path.split(refdir)[0]
p = path.normpath(path.expanduser(p))
if refdir is not None:
p = path.join(refdir, p)
return p
def get_tasksdir(self):
"""Retourner le répertoire de base pour les fichiers de tâches
"""
if self.has_key(self.TASKSDIR): return self.__p(self[self.TASKSDIR])
else: return self.dirname
def set_tasksdir(self, tasksdir=None):
if tasksdir is None:
if self.has_key(self.TASKSDIR): del self[self.TASKSDIR]
else:
self[self.TASKSDIR] = tasksdir
def get_projdirs(self):
"""Retourner la liste des répertoires de base de projets
"""
if self.has_key(self.PROJDIRS): return map(lambda p: self.__p(p, self.get_tasksdir()),
self.get_paths(self.PROJDIRS))
else: return ()
def set_projdirs(self, projdirs=None):
if projdirs is None:
if self.has_key(self.PROJDIRS): del self[self.PROJDIRS]
else:
if type(projdirs) is not StringType:
projdirs = path.pathsep.join(projdirs)
self[self.PROJDIRS] = projdirs
def get_tasksfile(self, default=_marker):
"""Retourner tasksfile s'il est défini, None sinon
"""
if self.has_key(self.TASKSFILE): return self.__p(self[self.TASKSFILE], self.get_tasksdir())
elif default is _marker: return None
else: return default
def get_taskssrc(self):
"""Retourner tasksfile s'il est défini, tasksdir sinon
"""
return self.get_tasksfile(self.get_tasksdir())
def set_tasksfile(self, tasksfile=None):
if tasksfile is None:
if self.has_key(self.TASKSFILE): del self[self.TASKSFILE]
else:
self[self.TASKSFILE] = tasksfile
def get_purgedfile(self, default=_marker):
"""Retourner purgedfile s'il est défini, None sinon
"""
if self.has_key(self.PURGEDFILE): return self.__p(self[self.PURGEDFILE], self.get_tasksdir())
elif default is _marker: return None
else: return default
def get_purgedsrc(self):
"""Retourner purgedfile s'il est défini, tasksdir sinon
"""
return self.get_purgedfile(self.get_tasksdir())
def set_purgedfile(self, purgedfile=None):
if purgedfile is None:
if self.has_key(self.PURGEDFILE): del self[self.PURGEDFILE]
else:
self[self.PURGEDFILE] = purgedfile
def get_done_color(self):
return self.get(self.DONE_COLOR, "faint")
def set_done_color(self, done_color):
self[self.DONE_COLOR] = done_color
def get_idea_color(self):
return self.get(self.IDEA_COLOR, "yellow")
def set_idea_color(self, idea_color):
self[self.IDEA_COLOR] = idea_color
def get_project_color(self):
return self.get(self.PROJECT_COLOR, "blue")
def set_project_color(self, project_color):
self[self.PROJECT_COLOR] = project_color
def get_context_color(self):
return self.get(self.CONTEXT_COLOR, "blue")
def set_context_color(self, context_color):
self[self.CONTEXT_COLOR] = context_color
def get_tag_color(self):
return self.get(self.TAG_COLOR, "faint,blue")
def set_tag_color(self, tag_color):
self[self.TAG_COLOR] = tag_color
class TasksCLI:
tasksrc = None
tasksfile = None
def __tf(self):
# retourner le fichier pour les tâches
if self.tasksfile is None:
tasksfile = self.tasksrc.get_tasksfile()
if tasksfile is not None: tasksfile = TextFile(tasksfile)
else: tasksfile = TextFile(self.tasksrc.get_taskssrc(), TextFile.TASKS_FILETYPE)
self.tasksfile = tasksfile
return self.tasksfile
purgedfile = None
def __pf(self):
# retourner le fichier pour les tâches purgées
if self.purgedfile is None:
purgedfile = self.tasksrc.get_purgedfile()
if purgedfile is not None: purgedfile = TextFile(purgedfile)
else: purgedfile = TextFile(self.tasksrc.get_purgedsrc(), TextFile.PURGED_FILETYPE)
self.purgedfile = purgedfile
return self.purgedfile
def __init__(self, tasksrc=None):
if not isinstance(tasksrc, TasksrcFile):
tasksrc = TasksrcFile(tasksrc)
self.tasksrc = tasksrc
CONFOPT = 'c:'
CONFLOPT = ['config=', 'tasksdir=', 'projdirs=', 'tasksfile=', 'purgedfile=']
def is_global_option(self, opt, value):
if opt in ('-c', '--config'):
self.tasksrc = TasksrcFile(value)
self.tasksfile = None
self.purgedfile = None
elif opt in ('--tasksdir', ):
self.tasksrc.set_tasksdir(value)
elif opt in ('--projdirs', ):
self.tasksrc.set_projdirs(value)
elif opt in ('--tasksfile', ):
self.tasksfile = None
self.tasksrc.set_tasksfile(value)
elif opt in ('--purgedfile', ):
self.purgedfile = None
self.tasksrc.set_purgedfile(value)
else:
return False
return True
def get_projdir(self, dir):
"""Si dir est un répertoire absolu, retourner le nom de projet
correspondant, ou None si ce n'est pas un répertoire de projet.
Sinon, retourner le répertoire absolu correspondant au répertoire de
projet
"""
projdirs = self.tasksrc.get_projdirs()
if path.isabs(dir):
# dir est un répertoire absolu. retourner le nom de projet
# correspondant
maxlen = 0
project = None
if dir not in projdirs:
for projdir in projdirs:
if dir.startswith(projdir + '/'):
p = dir[len(projdir) + 1:]
i = p.find('/')
if i != -1: p = p[:i]
pd = path.join(projdir, p)
if len(pd) > maxlen:
maxlen = len(pd)
project = p
return project
else:
# dir est un nom de projet. retourner le répertoire de projet
# correspondant
p = dir
for projdir in projdirs:
pd = path.join(projdir, p)
if path.isdir(pd): return pd
return None
def get_project(self):
"""Retourner le nom du projet si on est dans un répertoire de projet, None sinon
"""
return self.get_projdir(os.getcwd())
def is_project(self, project):
"""Retourner True si le project existe physiquement (le répertoire
correspondant est trouvé sur le disque.)
"""
return self.get_projdir(project) is not None
def get_tf(self, project):
"""Obtenir le fichier de tâche de project
"""
projdir = self.get_projdir(project)
if projdir is None:
return None
elif path.exists(path.join(projdir, 'TODO.html')):
return TWFile(projdir)
else:
return TextFile(projdir, TextFile.TASKS_FILETYPE)
def update_projects(self, delete=None, add_or_update=None, delete_projects=None, delete_ids=None):
"""Mettre à jour les tâches dans les répertoires de projet
delete est une liste de tâches qui doivent être supprimées dans les
répertoires de projet.
add_or_update est une liste de tâches qui doivent être ajoutées ou mises
à jour dans les répertoires de projet. delete_projects est la liste des
projets qui ont été supprimés de ces tâches. delete_ids est une liste
d'ids pour lesquels les tâches associées doivent être supprimées avant
d'être mises à jour.
On fait dans l'ordre: delete, delete_ids, add_or_update, delete_projects
"""
tfs = {}
def get_tf(p):
tf = tfs.get(p, None)
if tf is None:
tf = self.get_tf(p)
if tf is not None:
tfs[p] = tf
return tf
def remove_from_tf(p, t):
tf = get_tf(p)
if tf is not None: tf.remove(t.get_id())
def update_in_tf(p, t):
tf = get_tf(p)
if tf is not None:
pt = tf.get_by_id(t.get_id())
if pt is not None: pt.update_from(t)
else: tf.add(t)
def norm_seq(o):
if o is not None:
if type(o) not in (ListType, TupleType): o = (o, )
return o
delete = norm_seq(delete)
add_or_update = norm_seq(add_or_update)
delete_projects = norm_seq(delete_projects)
delete_ids = norm_seq(delete_ids)
if delete:
for t in delete:
for p in t.get_projects():
remove_from_tf(p, t)
if add_or_update:
if delete_ids:
for t in add_or_update:
for p in t.get_projects():
for id in delete_ids:
get_tf(p).remove(id)
for t in add_or_update:
for p in t.get_projects():
update_in_tf(p, t)
if delete_projects:
for p in delete_projects:
for t in add_or_update:
remove_from_tf(p, t)
for tf in tfs.values():
tf.save()
INDEX_PATTERN = re.compile(r'(\d+)$')
def __parse_index(self, index):
mo = self.INDEX_PATTERN.match(index)
if mo is not None: return int(mo.group(1))
else: return None
PROP_PATTERN = re.compile(r'(\S+)=(\S*)$')
def __parse_prop(self, prop):
mo = self.PROP_PATTERN.match(prop)
if mo is not None: return mo.group(1), mo.group(2)
else: return None, None
CONTEXT_PATTERN = re.compile(r'@(\S+)$')
def __parse_context(self, context):
mo = self.CONTEXT_PATTERN.match(context)
if mo is not None: return mo.group(1)
else: return None
PROJECT_PATTERN = re.compile(r'\+(\S+)$')
def __parse_project(self, project):
mo = self.PROJECT_PATTERN.match(project)
if mo is not None: return mo.group(1)
else: return None
def __get_filtered(self, tf, argv, auto_filter=True,
indexes=None, patterns=None, contexts=None, projects=None, props=None,
**kw):
if indexes is None: indexes = []
if props is None: props = kw
else: props.update(kw)
if contexts is None: contexts = []
if projects is None: projects = []
if patterns is None: patterns = []
auto_filtered = None
if not argv and auto_filter:
project = self.get_project()
if project is not None:
projects.append(project)
auto_filtered = project
else:
i = 0
for i in range(len(argv)):
arg = argv[i]
if arg == '--':
i += 1
break
index = self.__parse_index(arg)
name, value = self.__parse_prop(arg)
context = self.__parse_context(arg)
project = self.__parse_project(arg)
if index is not None: indexes.append(index)
elif name is not None: props[name] = value
elif context is not None: contexts.append(context)
elif project is not None: projects.append(project)
else: patterns.append(arg)
argv = argv[i:]
return tf.filter(indexes, projects, contexts, patterns, props), auto_filtered
def __cvs_commit(self, t, argv=()):
pass
############################################################################
def ADD(self,
type_option=None, done_option=False, edit_option=False, commit_option=False,
text=None, contexts=None, projects=None, props=None,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Créer une nouvelle tâche
USAGE
%(scriptname)s [-d] [-e] titre de la tâche
OPTIONS
-t Type de la tâche: TODO (par défaut) ou IDEA
-d Marquer la tâche comme faite après l'avoir créée. Ceci permet de suivre
une activité pour laquelle l'on n'avait pas créée un tâche au préalable.
Cette option est activée par défaut avec tt.
-e Editer la tâche après l'avoir créée. Ceci permet d'ajouter des
informations supplémentaires.
-C Faire 'cvs commit' avec le message de la tâche après avoir marqué la
tâche comme faite. Il n'est pas possible de passer des arguments à cvs
avec cette option. Cette option implique l'option -d"""
opts, argv = getopt(argv,
self.CONFOPT + 'ht:deC',
self.CONFLOPT + ['help', 'type=', 'done', 'edit', 'commit'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.ADD.__doc__ % locals())
sys.exit(0)
elif opt in ('-t', '--type'):
type_option = value
elif opt in ('-d', '--done'):
done_option = True
elif opt in ('-e', '--edit'):
edit_option = True
elif opt in ('-C', '--commit'):
done_option = True
commit_option = True
if text is None: text = ""
if contexts is None: contexts = []
if projects is None: projects = []
if props is None: props = kw
else: props.update(kw)
if argv:
for arg in argv:
name, value = self.__parse_prop(arg)
context = self.__parse_context(arg)
project = self.__parse_project(arg)
if name is not None: props[name] = value
elif context is not None: contexts.append(context)
elif project is not None: projects.append(project)
else:
if text != "": text += " "
text += arg
else:
# Si l'on n'a pas donné de texte, donner la possibilité de l'éditer.
edit_option = True
tf = self.__tf()
t = None
if props.has_key(ID):
tasks = tf.filter(id=props[ID])
if tasks:
t = tasks[0]
newtask = False
if t is None:
t = Task()
newtask = True
t.set_type(type_option)
t.update(props)
t.set_contexts(contexts)
t.set_projects(projects)
t.set_text(text)
if not t.has_date(): t.set_date()
if done_option: t.set_done()
if not t.has_projects():
project = self.get_project()
if project is not None: t.add_project(project)
if newtask: tf.add(t)
tf.save()
self.update_projects(add_or_update=t)
if argv: self.__print_task(t)
if edit_option: self.EDIT(t)
if commit_option: self.__cvs_commit(t)
############################################################################
def __print_task(self, t, width=1, showdesc=False):
parts = []
prefix_color = ''
reset_color = get_color("reset")
if t.is_todo_type():
if t.is_done():
prefix_color = get_color(self.tasksrc.get_done_color())
parts.append(prefix_color)
parts.append(u"[x]")
else:
parts.append(u"[ ]")
elif t.is_idea_type():
prefix_color = get_color(self.tasksrc.get_idea_color())
parts.append(prefix_color)
parts.append(u"Id>")
else:
prefix = parts.append(u"??>")
parts.append(u" %*i " % (width, t.index))
for p in t.get_projects():
parts.append(get_color(self.tasksrc.get_project_color()))
if t.is_project_purged(p): parts.append("-%s" % p)
else: parts.append("+%s" % p)
parts.append(reset_color)
parts.append(prefix_color)
parts.append(u" ")
for c in t.get_contexts():
parts.append(get_color(self.tasksrc.get_context_color()))
parts.append("@%s" % c)
parts.append(reset_color)
parts.append(prefix_color)
parts.append(u" ")
parts.append(t.get_title().replace('\n', ' '))
parts.append(reset_color)
uprint(''.join(parts))
desc = t.get_desc()
if showdesc:
if desc: uprint(desc)
tag_color = get_color(self.tasksrc.get_tag_color())
for name, value in t.items():
uprint(u"%s%s=%s%s" % (tag_color, name, value, reset_color))
def __print_stats(self, tasks, auto_filtered=None, show_only_todos=True):
numtodos = len(filter(lambda t: not t.is_done(), tasks))
numtasks = len(filter(lambda t: not t.is_todo_type(), tasks))
infos = []
if show_only_todos and numtasks != 0: infos.append(u"%i tâche(s) masquée(s)" % numtasks)
if auto_filtered: infos.append(u"filtrage auto +%s" % auto_filtered)
if infos: infos = u" [%s]" % u", ".join(infos)
else: infos = u""
enote(u"%i / %i tâche(s) trouvée(s)%s" % (numtodos, len(tasks), infos))
def LIST(self,
listall_option=False, listpurged_option=False, showdesc_option=False, showstats_option=True,
indexes=None, patterns=None, contexts=None, projects=None, props=None,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Lister les tâches en cours et leur état
USAGE
%(scriptname)s [-a] [-l] [--no-stats] filtres*|numeros*
OPTIONS
-a Lister aussi les tâches qui ne sont pas de type TODO
-p Lister aussi les tâche purgées
-l Afficher aussi la description et les tags associés aux tâches
--no-stats
Ne pas afficher de statistiques à la fin de la liste des tâches
Cette commande prend éventuellement une liste de filtres qui sont combinées
avec AND (les tâches listées doivent valider tous les filtres), sauf si on
ne donne que des numéros de tâches, auquel cas on liste simplement toutes
les tâches dont on a donné le numéro. Les filtre peuvent être des formes
suivantes:
+project
Pour filtrer uniquement les tâches d'un projet. Si on se trouve dans un
répertoire de projet, on filtre automatiquement sur ce projet. Si l'on
veut forcer le listage des tâches de tous les projets, il faut utiliser
la syntaxe +*
@context
Pour filtrer uniquement les tâches d'un contexte. Si l'on veut forcer
le listage des tâches de tous les contextes, il faut utiliser la syntaxe
@*
tag=value
Pour filtrer uniquement les tâches dont le tag existe et a la valeur
indiquée. La syntaxe tag=* permet de filtrer simplement sur l'existence
du tag. La syntaxe tag= permet de filtrer sur la NON existence du tag.
Les tags valides sont pour le moment id, date, done, ${project}_purged
$project est un nom de projet valide dans cette tâche.
<toute autre valeur>
Afficher les tâches qui contiennent les mots spécifiés."""
opts, argv = getopt(argv,
self.CONFOPT + 'hapln',
self.CONFLOPT + ['help', 'list-all', 'list-purged', 'show-desc', 'no-stats'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.LIST.__doc__ % locals())
sys.exit(0)
elif opt in ('-a', '--list-all'):
listall_option = True
elif opt in ('-p', '--list-purged'):
listpurged_option = True
elif opt in ('-l', '--show-desc'):
showdesc_option = True
elif opt in ('-n', '--no-stats'):
showstats_options = False
if listpurged_option:
tf = FileAggregate([self.__pf(), self.__tf()])
else:
tf = self.__tf()
tasks, auto_filtered = self.__get_filtered(tf, argv,
indexes=indexes,
patterns=patterns,
contexts=contexts,
projects=projects,
props=props)
if not auto_filtered:
# Si on effectue une recherche, afficher TOUS les résultats
listall_option = True
if tasks: width = len(str(max(map(lambda t: t.index, tasks))))
else: width = 1
for t in tasks:
if not listall_option and not t.is_todo_type(): continue
self.__print_task(t, width, showdesc=showdesc_option)
if showstats_option:
self.__print_stats(tasks, auto_filtered, not listall_option)
############################################################################
EDIT_TEMPLATE = u"""
EDIT: ----------------------------------------------------------------
EDIT: Saisissez ou modifiez le titre et la description de la tâche.
EDIT:
EDIT: - Les lignes commencant par 'EDIT:' seront supprimées automatiquement
EDIT: - Les lignes projects: et contexts: peuvent être modifiées si
EDIT: nécessaire.
EDIT:
EDIT: ----------------------------------------------------------------"""
CONTEXTS_PATTERN = re.compile(r'##\s*contexts:\s*')
CTXT_PATTERN = re.compile(r'(\S+)\s*')
PROJECTS_PATTERN = re.compile(r'##\s*projects:\s*')
PROJ_PATTERN = re.compile(r'(\S+)\s*')
PROP_PATTERN = re.compile(r'##\s*(\S+)\s*=\s*(\S+)')
def __nblines(self, s):
lines = s.split("\n")
nblines = len(lines)
if not lines[-1]: nblines -= 1
return nblines
def EDIT(self,
t=None,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: éditer une tâche dont on donne le numéro
USAGE
%(scriptname)s numero"""
opts, argv = getopt(argv,
self.CONFOPT + 'h',
self.CONFLOPT + ['help'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.EDIT.__doc__ % locals())
sys.exit(0)
tf = self.__tf()
if t is None:
count = 0
if argv:
tasks, auto_filtered = self.__get_filtered(tf, argv)
count = len(tasks)
if count > 0: t = tasks[0]
if count == 0:
error("Il faut spécifier un numéro de tâche à éditer")
return
elif count > 1:
ewarn("Seule la première tâche sera éditée")
template = u""
template += u"## contexts: %s\n" % " ".join(t.get_contexts())
template += u"## projects: %s\n" % " ".join(t.get_projects())
for name, value in t.items():
template += u"## %s=%s\n" % (name, value)
template += u"\n"
title = t.get_title()
template += u"%s\n" % title
setline = self.__nblines(template)
setcol = len(title)
desc = t.get_desc()
if desc: template += u"\n%s" % desc
template += self.EDIT_TEMPLATE
lines = edit_template(template, 'EDIT:', setline, setcol).split('\n')
new_contexts = []
new_projects = []
new_props = {}
parsing_tags = True
skip_empty = True
text = []
for line in lines:
if skip_empty and not line: continue
if parsing_tags:
moc = self.CONTEXTS_PATTERN.match(line)
mop = self.PROJECTS_PATTERN.match(line)
mo = self.PROP_PATTERN.match(line)
if moc is not None:
new_contexts.extend(map(t.ensure_unicode, self.CTXT_PATTERN.findall(line[moc.end():])))
continue
elif mop is not None:
new_projects.extend(map(t.ensure_unicode, self.PROJ_PATTERN.findall(line[mop.end():])))
continue
elif mo is not None:
name, value = mo.group(1), mo.group(2)
new_props[name] = t.ensure_unicode(value)
continue
else:
parsing_tags = False
skip_empty = False
text.append(line)
text = t.ensure_unicode("\n".join(text))
if t.get_contexts() != new_contexts: t.set_contexts(new_contexts)
delete_projects = []
if t.get_projects() != new_projects:
# Si la liste des projets est différente, il faudra supprimer les
# tâches des projets qui ne sont plus dans la liste
for p in t.get_projects():
if p not in new_projects:
delete_projects.append(p)
t.set_projects(new_projects)
delete_ids = []
if t.props != new_props:
if t.props[ID] != new_props.get(ID, None):
delete_ids.append(t.props[ID])
t.clear()
t.update(new_props)
if t.get_text() != text: t.set_text(text)
if t.is_modified():
if t.get_text():
self.__print_task(t)
tf.update_ids()
tf.save()
self.update_projects(add_or_update=t,
delete_projects=delete_projects,
delete_ids=delete_ids)
else:
# pas de texte, il faut supprimer la tâche
tf.remove(t)
tf.save()
self.update_projects(delete=t)
enote("Tâche supprimée")
else:
enote("Aucune modification effectuée")
############################################################################
def DO(self,
commit_option=False, force_option=False,
indexes=None, patterns=None, contexts=None, projects=None, props=None,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Marquer comme faites les tâches dont on donne le numéro
USAGE
%(scriptname)s numeros*
OPTIONS
-C Faire 'cvs commit' avec le message de la tâche après avoir marqué la
tâche comme faite. Dans ce cas, un seul numéro de tâche est autorisé.
Les autres arguments sont passés à cvs.
-f Faire le commit même si la tâche est déjà marquée comme faite."""
opts, argv = getopt(argv,
self.CONFOPT + 'hCf',
self.CONFLOPT + ['help', 'commit', 'force'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.DO.__doc__ % locals())
sys.exit(0)
elif opt in ('-C', '--commit'):
commit_option = True
elif opt in ('-f', '--force'):
force_option = True
if not argv:
eerror("Il faut spécifier un numéro de tâche à marquer comme faite")
return
tf = self.__tf()
tasks, auto_filtered = self.__get_filtered(tf, argv,
indexes=indexes,
patterns=patterns,
contexts=contexts,
projects=projects,
props=props)
saved = False
updated = []
for t in tasks:
done = t.is_done()
if not done: t.set_done()
if not t.is_todo_type(): t.set_type(None)
if t.is_modified():
saved = False
updated.append(t)
self.__print_task(t)
if commit_option and (force_option or not done):
tf.save()
saved = True
self.__cvs_commit(t, argv)
if updated:
if not saved: tf.save()
self.update_projects(add_or_update=updated)
############################################################################
def REMOVE(self,
indexes=None, patterns=None, contexts=None, projects=None, props=None,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Supprimer les tâches dont on donne le numéro
USAGE
%(scriptname)s numeros*"""
opts, argv = getopt(argv,
self.CONFOPT + 'h',
self.CONFLOPT + ['help'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.REMOVE.__doc__ % locals())
sys.exit(0)
if not argv:
eerror("Il faut spécifier un numéro de tâche à supprimer")
return
tf = self.__tf()
tasks, auto_filtered = self.__get_filtered(tf, argv,
indexes=indexes,
patterns=patterns,
contexts=contexts,
projects=projects,
props=props)
if len(tasks):
deleted = []
for t in tasks:
tf.remove(t)
deleted.append(t)
tf.save()
self.update_projects(delete=deleted)
enote("%i tâche(s) supprimée(s)" % len(tasks))
############################################################################
def PURGE(self,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Purger les tâches qui sont faites
USAGE
%(scriptname)s [projet]
OPTIONS
Si aucun projet n'est spécifié, les tâches qui sont marquées comme faites et
ne sont associées à aucun projet valide sont déplacées dans le fichier
d'archive.
Si un projet est spécifié, alors seules les tâches associées à ce projet
sont considérées. Si après avoir marqué le projet comme purgé, la tâche
n'est plus associée à aucun projet valide, la tâche est purgée: elle est
déplacée dans le fichier d'archive et le fichier DONE.txt du projet.
Le fichier d'archive est actuellement défini à $(ppath "$purgedfile")"""
opts, argv = getopt(argv,
self.CONFOPT + 'h',
self.CONFLOPT + ['help'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.PURGE.__doc__ % locals())
sys.exit(0)
projects = []
if not argv:
project = self.get_project()
if project is not None:
projects.append(project)
else:
projects.extend(argv)
tf = self.__tf()
pf = self.__pf()
now = datef()
updated = []
purged = []
for t in list(tf):
if not t.is_done(): continue
# la tâche est elligible à la purge
if projects:
# ne purger que les projets demandés
for project in projects:
t.set_project_purged(project, now)
else:
# purger tous les projets
for project in t.get_projects():
t.set_project_purged(project, now)
# si tous les projets sont purgés, on peut déplacer la ligne dans pf
if t.is_all_purged(self.is_project):
pf.add(t)
tf.remove(t)
purged.append(t)
else:
updated.append(t)
if purged:
pf.save()
tf.save()
self.update_projects(add_or_update=updated, delete=purged)
enote("%i tâche(s) purgée(s)" % len(purged))
self.LIST(showstats_option=False, props={'done': '*'})
############################################################################
def REPORT(self,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Afficher un rapport sur les tâches
USAGE
%(scriptname)s
OPTIONS
Afficher une liste des projets en cours, et leur pourcentage, ainsi que les
projets qui sont terminés (pour lesquels il n'y a pas de tâches."""
opts, argv = getopt(argv,
self.CONFOPT + 'h',
self.CONFLOPT + ['help'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.REPORT.__doc__ % locals())
sys.exit(0)
############################################################################
def UPDATE(self,
argv=(), scriptname=None,
**kw):
u"""%(scriptname)s: Forcer la mise à jour des fichiers de projet
USAGE
%(scriptname)s"""
opts, argv = getopt(argv,
self.CONFOPT + 'h',
self.CONFLOPT + ['help'])
for opt, value in opts:
if self.is_global_option(opt, value):
pass
elif opt in ('-h', '--help'):
uprint(self.UPDATE.__doc__ % locals())
sys.exit(0)
update = []
for t in self.__tf():
update.append(t)
self.update_projects(add_or_update=update)
################################################################################
if __name__ == '__main__':
action = None
argv = sys.argv[1:]
tasksCLI = TasksCLI()
# Essayer de determiner l'action avec le nom du script
if scriptname in ('ta', 'ti', 'tt'):
if scriptname == 'ti': argv[0:0] = ('-t', IDEA_TYPE)
if scriptname == 'tt': argv.insert(0, '-d')
action = 'add'
elif scriptname in ('tl', 'tll', 'tla'):
if scriptname == 'tll': argv.insert(0, '-l')
elif scriptname == 'tla': argv.insert(0, '-a')
action = 'list'
elif scriptname in ('te', ):
action = 'edit'
elif scriptname in ('td', 'tci'):
if scriptname == 'tci': argv.insert(0, '-C')
action = 'do'
if action is None:
opts, argv = getopt(argv,
TasksCLI.CONFOPT + 'h',
TasksCLI.CONFLOPT + ['help'])
for opt, value in opts:
if opt in ('-h', '--help'):
uprint(__doc__ % dict(scriptname=scriptname))
sys.exit(0)
elif tasksCLI.is_global_option(opt, value):
pass
if not argv:
uprint(__doc__ % dict(scriptname=scriptname))
sys.exit(0)
action, argv = argv[0], argv[1:]
if action in ('add', 'a', 'done'):
if action == 'done': argv.insert(0, '-d')
action = 'add'
elif action in ('list', 'l', 'll', 'la'):
if action == 'll': argv.insert(0, '-l')
elif action == 'la': argv.insert(0, '-a')
action = 'list'
elif action in ('edit', 'e'):
action = 'edit'
elif action in ('do', 'd', 'commit', 'c'):
if action in ('commit', 'c'): argv.insert(0, '-C')
action = 'do'
elif action in ('remove', 'r'):
action = 'remove'
elif action in ('purge', 'p'):
action = 'purge'
elif action in ('report', ):
action = 'report'
elif action in ('update', ):
action = 'update'
else:
eerror("Action inconnue: %s" % action)
sys.exit(1)
if scriptname in ('Tasks.py', 't'):
# pour l'affichage de l'aide
scriptname = '%s %s' % (scriptname, action)
apply(getattr(tasksCLI, action.upper()), (), {'argv': argv, 'scriptname': scriptname})