diff --git a/README.md b/README.md index 0a586ac..baf7cbb 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ Everything you need to know is located [here](https://epocdotfr.github.io/todotx See [here](https://github.com/EpocDotFr/todotxtio/releases). +## Contributors + +Thanks to: + + - [@Nnako](https://github.com/nnako) (Python v2 support) + ## End words If you have questions or problems, you can [submit an issue](https://github.com/EpocDotFr/todotxtio/issues). diff --git a/docs/index.rst b/docs/index.rst index abf8dce..e4a690e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ This module tries to comply to the `Todo.txt specifications 0: todo.projects = todo_projects text = todo_project_regex.sub('', text).strip() + # contexts todo_contexts = todo_context_regex.findall(text) - if len(todo_contexts) > 0: todo.contexts = todo_contexts text = todo_context_regex.sub('', text).strip() - todo_tags = todo_tag_regex.findall(text) + + + # + # evaluate links + # + + # http, https, link + todo_links = todo_filelink_regex.findall(text) + if todo_links: + todo.links = [] + for _prot, _path in todo_links: + + # check for colon + _idx = _prot.find(':') + + # build link entry + todo.links.append( + [ _prot[:_idx], _path ] + ) + + # remove identified content from text + text = todo_filelink_regex.sub('', text).strip() + + + + + # + # evaluate persons + # + + # responsibles + todo_responsibles = todo_responsibles_regex.findall(text) + if len(todo_responsibles) > 0: + todo.responsibles = todo_responsibles + text = todo_responsibles_regex.sub('', text).strip() + + # tobeinformed + todo_tobeinformed = todo_tobeinformed_regex.findall(text) + if len(todo_tobeinformed) > 0: + todo.tobeinformed = todo_tobeinformed + text = todo_tobeinformed_regex.sub('', text).strip() + + # authors + todo_authors = todo_authors_regex.findall(text) + if len(todo_authors) > 0: + todo.authors = todo_authors + text = todo_authors_regex.sub('', text).strip() + + + + + # + # evaluate further tags and text + # + + todo_tags = todo_tag_regex.findall(text) if len(todo_tags) > 0: for todo_tag in todo_tags: todo.tags[todo_tag[0]] = todo_tag[1] text = todo_tag_regex.sub('', text).strip() + # evaluate address + #if 'loc' in [_key.lower() for _key in todo.tags.keys()]: + #todo.tags['loc'] = todo.tags['loc'].replace('\\', '\n') + #todo.tags['loc'] = todo.tags['loc'].replace('_', ' ') + + # text todo.text = text + + + + # + # add this TODO to list of todos + # + todos.append(todo) return todos +# +# output functions +# + def to_dicts(todos): - """Convert a list of :class:`todotxtio.Todo` objects to a list of todo dict. + """ + Convert a list of :class:`todotxtio.Todo` objects to a list of todo dict. :param list todos: List of :class:`todotxtio.Todo` objects :rtype: list @@ -130,7 +310,8 @@ def to_dicts(todos): def to_stream(stream, todos, close=True): - """Write a list of todos to an already-opened stream. + """ + Write a list of todos to an already-opened stream. :param file stream: A file-like object :param list todos: List of :class:`todotxtio.Todo` objects @@ -144,37 +325,46 @@ def to_stream(stream, todos, close=True): def to_file(file_path, todos, encoding='utf-8'): - """Write a list of todos to a file. + """ + Write a list of todos to a file. :param str file_path: Path to the file :param list todos: List of :class:`todotxtio.Todo` objects :param str encoding: The encoding of the file to open :rtype: None """ - stream = open(file_path, 'w', encoding=encoding) + stream = io.open(file_path, 'w', encoding=encoding) to_stream(stream, todos) def to_string(todos): - """Convert a list of todos to a string. + """ + Convert a list of todos to a string. :param list todos: List of :class:`todotxtio.Todo` objects :rtype: str """ - return '\n'.join([str(todo) for todo in todos]) + return '\n'.join([serialize(todo) for todo in todos]) -class Todo: - """Represent one todo. +# +# main class definition +# - :param str text: The text of the todo - :param bool completed: Should this todo be marked as completed? - :param str completion_date: A date of completion, in the ``YYYY-MM-DD`` format. Setting this property will automatically set the ``completed`` attribute to ``True``. - :param str priority: The priority of the todo represented by a char between ``A-Z`` - :param str creation_date: A date of creation, in the ``YYYY-MM-DD`` format - :param list projects: A list of projects without leading ``+`` - :param list contexts: A list of projects without leading ``@`` - :param dict tags: A dict of tags +class Todo(object): + """ + Represent one todo. + + :param str text: The text of the todo + :param bool completed: Should this todo be marked as completed? + :param str completion_date: A date of completion, in the ``YYYY-MM-DD`` format. + Setting this property will automatically set the + ``completed`` attribute to ``True``. + :param str priority: The priority of the todo represented by a char between ``A-Z`` + :param str creation_date: A date of creation, in the ``YYYY-MM-DD`` format + :param list projects: A list of projects without leading ``+`` + :param list contexts: A list of projects without leading ``@`` + :param dict tags: A dict of tags """ text = None completed = False @@ -184,8 +374,26 @@ class Todo: projects = [] contexts = [] tags = {} - - def __init__(self, text=None, completed=False, completion_date=None, priority=None, creation_date=None, projects=None, contexts=None, tags=None): + remarks = [] + authors = [] + responsibles = [] + tobeinformed = [] + links = [] + + def __init__(self, + text=None, + completed=False, + completion_date=None, + priority=None, + creation_date=None, + projects=None, + contexts=None, + tags=None, + remarks=None, + authors=None, + responsibles=None, + tobeinformed=None, + ): self.text = text self.completed = completed @@ -197,9 +405,14 @@ def __init__(self, text=None, completed=False, completion_date=None, priority=No self.projects = projects self.contexts = contexts self.tags = tags + self.remarks = remarks + self.authors = authors + self.responsibles = responsibles + self.tobeinformed = tobeinformed def to_dict(self): - """Return a dict representation of this Todo instance. + """ + Return a dict representation of this Todo instance. :rtype: dict """ @@ -212,34 +425,57 @@ def to_dict(self): 'projects': self.projects, 'contexts': self.contexts, 'tags': self.tags, + 'remarks': self.remarks, + 'authors': self.authors, + 'responsibles': self.responsibles, + 'tobeinformed': self.tobeinformed, + 'links': self.links, } def __setattr__(self, name, value): + + # BOOL TYPE if name == 'completed': if not value: - super().__setattr__('completion_date', None) # Uncompleted todo must not have any completion date + super(Todo, self).__setattr__('completion_date', None) # Uncompleted todo must not have any completion date + + # DATE TYPE elif name == 'completion_date': if value: - super().__setattr__('completed', True) # Setting the completion date must set this todo as completed... + super(Todo, self).__setattr__('completed', True) # Setting the completion date must set this todo as completed... else: - super().__setattr__('completed', False) # ...and vice-versa - elif name in ['projects', 'contexts']: + super(Todo, self).__setattr__('completed', False) # ...and vice-versa + + # STRING TYPE + elif name in ['remarks']: if not value: - super().__setattr__(name, []) # Force contexts, projects to be lists when setting them to a falsely value + super(Todo, self).__setattr__(name, '') # Force contexts, projects to be lists when setting them to a falsely value + return + #elif type(value) is not str: + #raise ValueError(name + ' should be a string') + + # LIST TYPE + elif name in ['projects', 'contexts', 'authors', 'responsibles', 'tobeinformed']: + if not value: + super(Todo, self).__setattr__(name, []) # Force contexts, projects to be lists when setting them to a falsely value return elif type(value) is not list: # Make sure, otherwise, that the provided value is a list raise ValueError(name + ' should be a list') + + # TAG TYPE elif name == 'tags': if not value: - super().__setattr__(name, {}) # Force tags to be a dict when setting them to a falsely value + super(Todo, self).__setattr__(name, {}) # Force tags to be a dict when setting them to a falsely value return elif type(value) is not dict: # Make sure, otherwise, that the provided value is a dict raise ValueError(name + ' should be a dict') - super().__setattr__(name, value) + super(Todo, self).__setattr__(name, value) def __str__(self): - """Convert this Todo object in a valid Todo.txt line.""" + """ + Convert this Todo object in a valid Todo.txt line. + """ ret = [] if self.completed: @@ -268,33 +504,63 @@ def __str__(self): return ' '.join(ret) def __repr__(self): - """Call the __str__ method to return a textual representation of this Todo object.""" + """ + Call the __str__ method to return a textual representation of this Todo object. + """ return self.__str__() -def search(todos, text=None, completed=None, completion_date=None, priority=None, creation_date=None, projects=None, contexts=None, tags=None): - """Return a list of todos that matches the provided filters. +# +# search and format functions +# + +def search(todos, + text=None, + completed=None, + completion_date=None, + priority=None, + creation_date=None, + projects=None, + contexts=None, + tags=None, + remarks=None, + authors=None, + responsible=None, + tobeinformed=None, + links=None, + ): + """ + Return a list of todos that matches the provided filters. - It takes the exact same parameters as the :class:`todotxtio.Todo` object constructor, and return a list of :class:`todotxtio.Todo` objects as well. + It takes the exact same parameters as the :class:`todotxtio.Todo` + object constructor, and return a list of :class:`todotxtio.Todo` objects as well. All criteria defaults to ``None`` which means that the criteria is ignored. - A todo will be returned in the results list if all of the criteria matches. From the moment when a todo is sent in the results list, it will - never be checked again. + A todo will be returned in the results list if all of the criteria matches. From + the moment when a todo is sent in the results list, it will never be checked again. - :param str text: String to be found in the todo text - :param bool completed: Search for completed/uncompleted todos only + :param str text: String to be found in the todo text + :param bool completed: Search for completed/uncompleted todos only :param str completion_date: Match this completion date - :param list priority: List of priorities to match - :param str creation_date: Match this creation date - :param list projects: List of projects to match - :param list contexts: List of contexts to match - :param dict tags: Dict of tag to match + :param list priority: List of priorities to match + :param str creation_date: Match this creation date + :param list projects: List of projects to match + :param list contexts: List of contexts to match + :param dict tags: Dict of tag to match :rtype: list """ results = [] for todo in todos: - text_match = completed_match = completion_date_match = priority_match = creation_date_match = projects_match = contexts_match = tags_match =True + text_match \ + = completed_match \ + = completion_date_match \ + = priority_match \ + = creation_date_match \ + = projects_match \ + = contexts_match \ + = tags_match \ + = True if text is not None: text_match = text in todo.text @@ -320,7 +586,115 @@ def search(todos, text=None, completed=None, completion_date=None, priority=None if tags is not None: tags_match = any(todo.tags[k] == v for k, v in tags.items() if k in todo.tags) - if text_match and completed_match and completion_date_match and priority_match and creation_date_match and projects_match and contexts_match and tags_match: + if text_match \ + and completed_match \ + and completion_date_match \ + and priority_match \ + and creation_date_match \ + and projects_match \ + and contexts_match \ + and tags_match: results.append(todo) return results + + +def serialize(todo): + """ + Convert a Todo object in a serial Todo.txt line. + """ + + # in Python v2 there seems to be a problem with __str__ and non-standard + # string characters (as they are encountered e.g. in German languages). + # __str__ seems to return only regular string characters. + + ret = [] + + + + + # + # create prefix + # + + if todo.completed: + ret.append('x') + + if todo.completion_date: + ret.append(todo.completion_date) + + if todo.priority: + ret.append('(' + todo.priority + ')') + + if todo.creation_date: + ret.append(todo.creation_date) + + + + + # + # append text + # + + ret.append(todo.text) + + + + + # + # append remarks + # + + if todo.remarks: + #ret.append(''.join([' {' + remarks + '}' for remarks in todo.remarks]).strip()) + ret.append(' {' + todo.remarks.replace('\n', '\\').strip() + '}') + + + + + # + # append links + # + + if todo.links: + for _prot, _path in todo.links: + if _prot in [ 'link' ]: + ret.append((' ' + _prot + ':' + _path).strip()) + else: + ret.append((' ' + _prot + '://' + _path).strip()) + + + + + # + # append projects, contexts and tags + # + + if todo.projects: + ret.append(''.join([' +' + project for project in todo.projects]).strip()) + + if todo.contexts: + ret.append(''.join([' @' + context for context in todo.contexts]).strip()) + + if todo.tags: + ret.append(''.join([' ' + tag_name + ':' + tag_value for tag_name, tag_value in todo.tags.items()]).strip()) + + + + # + # append persons + # + + if todo.authors: + ret.append(''.join([' [*' + auth + ']' for auth in todo.authors]).strip()) + + if todo.responsibles: + ret.append(''.join([' [' + resp + ']' for resp in todo.responsibles]).strip()) + + if todo.tobeinformed: + ret.append(''.join([' [+' + info + ']' for info in todo.tobeinformed]).strip()) + + + + + return ' '.join(ret)