From deb9dc2d9f0d35663aab68ca55143f1f054b0027 Mon Sep 17 00:00:00 2001 From: nnako Date: Thu, 29 Mar 2018 21:11:07 +0200 Subject: [PATCH 01/20] FIX: compatibility with Python v2.x MOD: some cosmetics regarding folding of function comments --- todotxtio.py | 105 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 7bdaa35..efe951a 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -1,5 +1,9 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + import os import re +import io __version__ = '0.2.2' @@ -16,14 +20,20 @@ 'search' ] -todo_data_regex = re.compile('^(?:(x) )?(?:(\d{4}-\d{2}-\d{2}) )?(?:\(([A-Z])\) )?(?:(\d{4}-\d{2}-\d{2}) )?') +todo_data_regex = re.compile( \ + '^(?:(x) )?' + \ + '(?:(\d{4}-\d{2}-\d{2}) )?' + \ + '(?:\(([A-Z])\) )?' + \ + '(?:(\d{4}-\d{2}-\d{2}) )?' \ + ) todo_project_regex = re.compile(' \+(\S*)') todo_context_regex = re.compile(' @(\S*)') todo_tag_regex = re.compile(' (\S*):(\S*)') def from_dicts(todos): - """Convert a list of todo dicts to a list of :class:`todotxtio.Todo` objects. + """ + Convert a list of todo dicts to a list of :class:`todotxtio.Todo` objects. :param list todos: A list of todo dicts :rtype: list @@ -32,7 +42,8 @@ def from_dicts(todos): def from_stream(stream, close=True): - """Load a todo list from an already-opened stream. + """ + Load a todo list from an already-opened stream. :param file stream: A file-like object :param bool close: Whetever to close the stream or not after all operation are finised @@ -47,7 +58,8 @@ def from_stream(stream, close=True): def from_file(file_path, encoding='utf-8'): - """Load a todo list from a file. + """ + Load a todo list from a file. :param str file_path: Path to the file :param str encoding: The encoding of the file to open @@ -56,26 +68,43 @@ def from_file(file_path, encoding='utf-8'): if not os.path.isfile(file_path): raise FileNotFoundError('File doesn\'t exists: ' + file_path) - stream = open(file_path, 'r', encoding=encoding) + stream = io.open(file_path, 'r', encoding=encoding) return from_stream(stream) def from_string(string): - """Load a todo list from a string. + """ + Load a todo list from a string. :param str string: The string to parse :rtype: list """ + + # init todos = [] + + + # + # evaluate each line + # + for line in string.strip().splitlines(): + line = line.strip() todo_pre_data = todo_data_regex.match(line) todo = Todo() + + + + # + # evaluate prefix data + # + if todo_pre_data: todo.completed = todo_pre_data.group(1) == 'x' @@ -93,35 +122,49 @@ def from_string(string): else: text = line - todo_projects = todo_project_regex.findall(text) + + # + # evaluate remaining data + # + + # projects + todo_projects = todo_project_regex.findall(text) if len(todo_projects) > 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() + # tags 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() + # text todo.text = text + + + # + # add this TODO to list of todos + # + todos.append(todo) return todos 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 +173,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,19 +188,21 @@ 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 @@ -164,8 +210,9 @@ def to_string(todos): return '\n'.join([str(todo) for todo in todos]) -class Todo: - """Represent one todo. +class Todo(object): + """ + Represent one todo. :param str text: The text of the todo :param bool completed: Should this todo be marked as completed? @@ -199,7 +246,8 @@ def __init__(self, text=None, completed=False, completion_date=None, priority=No self.tags = tags def to_dict(self): - """Return a dict representation of this Todo instance. + """ + Return a dict representation of this Todo instance. :rtype: dict """ @@ -217,29 +265,31 @@ def to_dict(self): def __setattr__(self, name, value): 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 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 + super(Todo, self).__setattr__('completed', False) # ...and vice-versa elif name in ['projects', 'contexts']: 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 list: # Make sure, otherwise, that the provided value is a list raise ValueError(name + ' should be a list') 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,12 +318,15 @@ 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. + """ + 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. All criteria defaults to ``None`` which means that the criteria is ignored. From b9d9e7a049e9f84ca185c040900b4d47de03aea2 Mon Sep 17 00:00:00 2001 From: nnako Date: Thu, 29 Mar 2018 23:47:10 +0200 Subject: [PATCH 02/20] FIX: the CREATION DATE will always appear in the 4th group of the defined regex --- todotxtio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index efe951a..7f0a89d 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -114,7 +114,7 @@ def from_string(string): if todo_pre_data.group(2): todo.completion_date = todo_pre_data.group(2) else: - todo.creation_date = todo_pre_data.group(2) + todo.creation_date = todo_pre_data.group(4) todo.priority = todo_pre_data.group(3) From 5f5af12f03233aa00d9792667754d0af7e2b1677 Mon Sep 17 00:00:00 2001 From: nnako Date: Fri, 30 Mar 2018 10:06:23 +0200 Subject: [PATCH 03/20] removed some comments --- todotxtio.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 7f0a89d..d1478cd 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- import os @@ -80,12 +79,11 @@ def from_string(string): :param str string: The string to parse :rtype: list """ - - # init todos = [] + # # evaluate each line # @@ -100,7 +98,6 @@ def from_string(string): - # # evaluate prefix data # From ec2f2054dc1d2703372b4c9111c1a6a5ef9e506e Mon Sep 17 00:00:00 2001 From: nnako Date: Fri, 30 Mar 2018 10:22:07 +0200 Subject: [PATCH 04/20] update of documentation and Python version info --- README.md | 6 ++++++ docs/index.rst | 2 +- setup.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) 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 Date: Sat, 31 Mar 2018 11:41:41 +0200 Subject: [PATCH 05/20] NEW: PERSON specifications by using different SQUARE BRACKET formats NEW: REMARKS specification by using CURLY BRACKETS --- todotxtio.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index d1478cd..e8ddc78 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -28,6 +28,10 @@ todo_project_regex = re.compile(' \+(\S*)') todo_context_regex = re.compile(' @(\S*)') todo_tag_regex = re.compile(' (\S*):(\S*)') +todo_author_regex = re.compile(' \[\*(\S*)\]') +todo_responsible_regex = re.compile(' \[([^\+\*\s]*)\]') +todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') +todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') def from_dicts(todos): @@ -83,7 +87,6 @@ def from_string(string): - # # evaluate each line # @@ -122,7 +125,7 @@ def from_string(string): # - # evaluate remaining data + # evaluate contexts and projects # # projects @@ -137,7 +140,47 @@ def from_string(string): todo.contexts = todo_contexts text = todo_context_regex.sub('', text).strip() - # tags + + + # + # evaluate persons + # + + # responsible + todo_responsible = todo_responsible_regex.findall(text) + if len(todo_responsible) > 0: + todo.responsible = todo_responsible + text = todo_responsible_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() + + # author + todo_author = todo_author_regex.findall(text) + if len(todo_author) > 0: + todo.author = todo_author + text = todo_author_regex.sub('', text).strip() + + + + # + # evaluate remarks + # + + todo_remarks = todo_remarks_regex.findall(text) + if len(todo_remarks) > 0: + todo.remarks = todo_remarks + text = todo_remarks_regex.sub('', text).strip() + + + + # + # evaluate further tags + # + todo_tags = todo_tag_regex.findall(text) if len(todo_tags) > 0: for todo_tag in todo_tags: @@ -228,8 +271,25 @@ class Todo(object): 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 = None + author = None + responsible = None + tobeinformed = [] + + def __init__(self, + text=None, + completed=False, + completion_date=None, + priority=None, + creation_date=None, + projects=None, + contexts=None, + tags=None, + remarks=None, + author=None, + responsible=None, + tobeinformed=[], + ): self.text = text self.completed = completed @@ -241,6 +301,10 @@ 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.author = author + self.responsible = responsible + self.tobeinformed = tobeinformed def to_dict(self): """ @@ -257,6 +321,10 @@ def to_dict(self): 'projects': self.projects, 'contexts': self.contexts, 'tags': self.tags, + 'remarks': self.remarks, + 'author': self.author, + 'responsible': self.responsible, + 'tobeinformed': self.tobeinformed, } def __setattr__(self, name, value): @@ -321,7 +389,20 @@ def __repr__(self): return self.__str__() -def search(todos, text=None, completed=None, completion_date=None, priority=None, creation_date=None, projects=None, contexts=None, tags=None): +def search(todos, + text=None, + completed=None, + completion_date=None, + priority=None, + creation_date=None, + projects=None, + contexts=None, + tags=None, + remarks=None, + author=None, + responsible=None, + tobeinformed=[], + ): """ Return a list of todos that matches the provided filters. From 71b6937b20075e0a325d78e671c37573e866fc70 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Wed, 24 Jul 2019 18:15:07 +0200 Subject: [PATCH 06/20] some minimal comment changes --- todotxtio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index e8ddc78..d4a02e6 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -1,3 +1,4 @@ +#!/usr/bin/python # -*- coding: utf-8 -*- import os @@ -33,7 +34,6 @@ todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') - def from_dicts(todos): """ Convert a list of todo dicts to a list of :class:`todotxtio.Todo` objects. @@ -275,6 +275,7 @@ class Todo(object): author = None responsible = None tobeinformed = [] + remarks = None def __init__(self, text=None, From f84d5bf53c6cd3673aa596b6ba205b95035dc5b6 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Wed, 24 Jul 2019 18:18:11 +0200 Subject: [PATCH 07/20] FIX: usage of serialize() instead of __str__() so in Python v2 unicode can be handled FIX: __setattr__() function now handles persons and remarks as well MOD: rename "author" -> "authors" and "responsible" -> "responsibles" as they are lists --- todotxtio.py | 143 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 28 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index d4a02e6..a7c7ebb 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- import os @@ -29,11 +28,12 @@ todo_project_regex = re.compile(' \+(\S*)') todo_context_regex = re.compile(' @(\S*)') todo_tag_regex = re.compile(' (\S*):(\S*)') -todo_author_regex = re.compile(' \[\*(\S*)\]') -todo_responsible_regex = re.compile(' \[([^\+\*\s]*)\]') +todo_authors_regex = re.compile(' \[\*(\S*)\]') +todo_responsibles_regex = re.compile(' \[([^\+\*\s]*)\]') todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') + def from_dicts(todos): """ Convert a list of todo dicts to a list of :class:`todotxtio.Todo` objects. @@ -146,11 +146,11 @@ def from_string(string): # evaluate persons # - # responsible - todo_responsible = todo_responsible_regex.findall(text) - if len(todo_responsible) > 0: - todo.responsible = todo_responsible - text = todo_responsible_regex.sub('', text).strip() + # 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) @@ -158,11 +158,11 @@ def from_string(string): todo.tobeinformed = todo_tobeinformed text = todo_tobeinformed_regex.sub('', text).strip() - # author - todo_author = todo_author_regex.findall(text) - if len(todo_author) > 0: - todo.author = todo_author - text = todo_author_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() @@ -247,7 +247,7 @@ def to_string(todos): :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(object): @@ -271,11 +271,10 @@ class Todo(object): projects = [] contexts = [] tags = {} - remarks = None - author = None - responsible = None + remarks = [] + authors = [] + responsibles = [] tobeinformed = [] - remarks = None def __init__(self, text=None, @@ -287,9 +286,9 @@ def __init__(self, contexts=None, tags=None, remarks=None, - author=None, - responsible=None, - tobeinformed=[], + authors=None, + responsibles=None, + tobeinformed=None, ): self.text = text self.completed = completed @@ -303,8 +302,8 @@ def __init__(self, self.contexts = contexts self.tags = tags self.remarks = remarks - self.author = author - self.responsible = responsible + self.authors = authors + self.responsibles = responsibles self.tobeinformed = tobeinformed def to_dict(self): @@ -323,26 +322,34 @@ def to_dict(self): 'contexts': self.contexts, 'tags': self.tags, 'remarks': self.remarks, - 'author': self.author, - 'responsible': self.responsible, + 'authors': self.authors, + 'responsibles': self.responsibles, 'tobeinformed': self.tobeinformed, } def __setattr__(self, name, value): + + # BOOL TYPE if name == 'completed': if not value: super(Todo, self).__setattr__('completion_date', None) # Uncompleted todo must not have any completion date + + #DATE TYPE elif name == 'completion_date': if value: super(Todo, self).__setattr__('completed', True) # Setting the completion date must set this todo as completed... else: super(Todo, self).__setattr__('completed', False) # ...and vice-versa - elif name in ['projects', 'contexts']: + + # LIST TYPE + elif name in ['projects', 'contexts', 'authors', 'responsibles', 'tobeinformed', 'remarks']: 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(Todo, self).__setattr__(name, {}) # Force tags to be a dict when setting them to a falsely value @@ -400,9 +407,9 @@ def search(todos, contexts=None, tags=None, remarks=None, - author=None, + authors=None, responsible=None, - tobeinformed=[], + tobeinformed=None, ): """ Return a list of todos that matches the provided filters. @@ -456,3 +463,83 @@ def search(todos, 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()) + + + + # + # 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) From ac5c86f1657898703a5033d6e7e6dd2044d1bdfa Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Wed, 24 Jul 2019 18:24:25 +0200 Subject: [PATCH 08/20] FIX: facilitate normal time string "12:34" within text by forcing starting alpha char for attributes --- todotxtio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index a7c7ebb..385226f 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -27,7 +27,7 @@ ) todo_project_regex = re.compile(' \+(\S*)') todo_context_regex = re.compile(' @(\S*)') -todo_tag_regex = re.compile(' (\S*):(\S*)') +todo_tag_regex = re.compile(' ([A-z]\S*):(\S*)') todo_authors_regex = re.compile(' \[\*(\S*)\]') todo_responsibles_regex = re.compile(' \[([^\+\*\s]*)\]') todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') From e46fc3a6f8c0ec445e38588f5a2fb905facf28b1 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Mon, 2 Sep 2019 17:48:43 +0200 Subject: [PATCH 09/20] FIX: occasional creation date mismatch corrected on import of TODOTXT file --- todotxtio.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 385226f..3a78f78 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -87,6 +87,7 @@ def from_string(string): + # # evaluate each line # @@ -101,6 +102,7 @@ def from_string(string): + # # evaluate prefix data # @@ -114,7 +116,10 @@ def from_string(string): if todo_pre_data.group(2): todo.completion_date = todo_pre_data.group(2) else: - todo.creation_date = todo_pre_data.group(4) + if todo_pre_data.group(4): + todo.creation_date = todo_pre_data.group(4) + else: + todo.creation_date = todo_pre_data.group(2) todo.priority = todo_pre_data.group(3) @@ -124,6 +129,7 @@ def from_string(string): + # # evaluate contexts and projects # @@ -142,6 +148,7 @@ def from_string(string): + # # evaluate persons # @@ -166,6 +173,7 @@ def from_string(string): + # # evaluate remarks # @@ -177,8 +185,9 @@ def from_string(string): + # - # evaluate further tags + # evaluate further tags and text # todo_tags = todo_tag_regex.findall(text) @@ -188,11 +197,17 @@ def from_string(string): 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 # @@ -334,7 +349,7 @@ def __setattr__(self, name, value): if not value: super(Todo, self).__setattr__('completion_date', None) # Uncompleted todo must not have any completion date - #DATE TYPE + # DATE TYPE elif name == 'completion_date': if value: super(Todo, self).__setattr__('completed', True) # Setting the completion date must set this todo as completed... @@ -464,6 +479,7 @@ def search(todos, return results + def serialize(todo): """ Convert a Todo object in a serial Todo.txt line. @@ -477,6 +493,7 @@ def serialize(todo): + # # create prefix # @@ -495,6 +512,7 @@ def serialize(todo): + # # append text # @@ -503,6 +521,7 @@ def serialize(todo): + # # append remarks # @@ -512,6 +531,7 @@ def serialize(todo): + # # append projects, contexts and tags # @@ -542,4 +562,5 @@ def serialize(todo): + return ' '.join(ret) From dae5165486f12abb6f418e722f468a16bc4814d1 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Tue, 3 Sep 2019 20:21:23 +0200 Subject: [PATCH 10/20] FIX: return REMARKS not as list but as string containing '\n' --- todotxtio.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 3a78f78..c974de3 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -178,9 +178,14 @@ def from_string(string): # evaluate remarks # + # get all remark portions as a list of strings todo_remarks = todo_remarks_regex.findall(text) - if len(todo_remarks) > 0: - todo.remarks = todo_remarks + if todo_remarks: + # concatenate portions + todo_remarks = '\\'.join(todo_remarks) + # translate LINE BREAKS + todo.remarks = todo_remarks.replace('\\','\n') + # remove all remark portions from text text = todo_remarks_regex.sub('', text).strip() @@ -356,8 +361,16 @@ def __setattr__(self, name, value): else: super(Todo, self).__setattr__('completed', False) # ...and vice-versa + # STRING TYPE + elif name in ['remarks']: + 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 str: + raise ValueError(name + ' should be a string') + # LIST TYPE - elif name in ['projects', 'contexts', 'authors', 'responsibles', 'tobeinformed', 'remarks']: + 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 @@ -527,7 +540,8 @@ def serialize(todo): # if todo.remarks: - ret.append(''.join([' {' + remarks + '}' for remarks in todo.remarks]).strip()) + #ret.append(''.join([' {' + remarks + '}' for remarks in todo.remarks]).strip()) + ret.append(' {' + todo.remarks.replace('\n', '\\').strip() + '}') From c878bf774134ecd8902701dc6a4662b19f89d766 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Tue, 3 Sep 2019 22:26:35 +0200 Subject: [PATCH 11/20] FIX: remove warning message for REMARK to be no string --- todotxtio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index c974de3..4c3afcb 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -366,8 +366,8 @@ def __setattr__(self, name, value): 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 str: - raise ValueError(name + ' should be a string') + #elif type(value) is not str: + #raise ValueError(name + ' should be a string') # LIST TYPE elif name in ['projects', 'contexts', 'authors', 'responsibles', 'tobeinformed']: From 0bda3de0586284fc539bf41e8de81231a6c881fa Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Mon, 9 Sep 2019 18:30:46 +0200 Subject: [PATCH 12/20] FIX: evaluate REMARKS before PERSONS to facilitate square brackets within remarks --- todotxtio.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 4c3afcb..7e0074e 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -149,6 +149,23 @@ def from_string(string): + # + # evaluate remarks + # + + # get all remark portions as a list of strings + todo_remarks = todo_remarks_regex.findall(text) + if todo_remarks: + # concatenate portions + todo_remarks = '\\'.join(todo_remarks) + # translate LINE BREAKS + todo.remarks = todo_remarks.replace('\\','\n') + # remove all remark portions from text + text = todo_remarks_regex.sub('', text).strip() + + + + # # evaluate persons # @@ -174,23 +191,6 @@ def from_string(string): - # - # evaluate remarks - # - - # get all remark portions as a list of strings - todo_remarks = todo_remarks_regex.findall(text) - if todo_remarks: - # concatenate portions - todo_remarks = '\\'.join(todo_remarks) - # translate LINE BREAKS - todo.remarks = todo_remarks.replace('\\','\n') - # remove all remark portions from text - text = todo_remarks_regex.sub('', text).strip() - - - - # # evaluate further tags and text # From 11080b6133fa47349ece9e93bb73b7ed9f298fbb Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Mon, 25 Nov 2019 12:12:05 +0100 Subject: [PATCH 13/20] NEW: prioritized evaluation of specific link attributes (http, https, link) into list of list --- todotxtio.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/todotxtio.py b/todotxtio.py index 7e0074e..86abe0e 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -31,6 +31,7 @@ todo_authors_regex = re.compile(' \[\*(\S*)\]') todo_responsibles_regex = re.compile(' \[([^\+\*\s]*)\]') todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') +todo_filelink_regex = re.compile(' (http://|https://|link:)(\S*)') todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') @@ -166,6 +167,30 @@ def from_string(string): + # + # 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 # @@ -295,6 +320,7 @@ class Todo(object): authors = [] responsibles = [] tobeinformed = [] + links = [] def __init__(self, text=None, @@ -345,6 +371,7 @@ def to_dict(self): 'authors': self.authors, 'responsibles': self.responsibles, 'tobeinformed': self.tobeinformed, + 'links': self.links, } def __setattr__(self, name, value): @@ -438,6 +465,7 @@ def search(todos, authors=None, responsible=None, tobeinformed=None, + links=None, ): """ Return a list of todos that matches the provided filters. @@ -546,6 +574,20 @@ def serialize(todo): + # + # 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 # From 983b3a2f47c399761d4857f5bac2a4910348ff83 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Sat, 18 Apr 2020 23:40:27 +0200 Subject: [PATCH 14/20] FIX: for detection of tags, take tag name until FIRST colon char --- todotxtio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index 86abe0e..97a67cc 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -27,7 +27,8 @@ ) todo_project_regex = re.compile(' \+(\S*)') todo_context_regex = re.compile(' @(\S*)') -todo_tag_regex = re.compile(' ([A-z]\S*):(\S*)') +# todo_tag_regex = re.compile(' ([A-z]\S*):(\S*)') +todo_tag_regex = re.compile(' ([A-z]\S*?):(\S*)') todo_authors_regex = re.compile(' \[\*(\S*)\]') todo_responsibles_regex = re.compile(' \[([^\+\*\s]*)\]') todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') From 36cadce7580be53f777fcd434374c3a58d4949a9 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Sat, 12 Dec 2020 15:10:43 +0100 Subject: [PATCH 15/20] added better comments --- todotxtio.py | 106 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 97a67cc..036f661 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -4,7 +4,7 @@ import re import io -__version__ = '0.2.2' +__version__ = '1.1.1' __all__ = [ 'from_dicts', @@ -19,22 +19,52 @@ 'search' ] + +# +# regular expressions +# + +# all of a text line's elements are identified using regular expressions. these +# regular expressions definitions will be found below. please note that not +# every specific term has to be defined, as "tag" attributes hold every +# non-list elements defined by the general "colon" syntax. + +# line prefix data todo_data_regex = re.compile( \ '^(?:(x) )?' + \ '(?:(\d{4}-\d{2}-\d{2}) )?' + \ '(?:\(([A-Z])\) )?' + \ '(?:(\d{4}-\d{2}-\d{2}) )?' \ ) + +# project and subproject names todo_project_regex = re.compile(' \+(\S*)') + +# context and subcontext names todo_context_regex = re.compile(' @(\S*)') -# todo_tag_regex = re.compile(' ([A-z]\S*):(\S*)') -todo_tag_regex = re.compile(' ([A-z]\S*?):(\S*)') + +# author names todo_authors_regex = re.compile(' \[\*(\S*)\]') + +# responsible person names todo_responsibles_regex = re.compile(' \[([^\+\*\s]*)\]') + +# names of regarded persons todo_tobeinformed_regex = re.compile(' \[\+(\S*)\]') + +# file and hyperlinks todo_filelink_regex = re.compile(' (http://|https://|link:)(\S*)') + +# text block of remarks todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') +# all other information as tags +todo_tag_regex = re.compile(' ([A-z]\S*?):(\S*)') + + +# +# input functions +# def from_dicts(todos): """ @@ -248,6 +278,10 @@ def from_string(string): return todos +# +# output functions +# + def to_dicts(todos): """ Convert a list of :class:`todotxtio.Todo` objects to a list of todo dict. @@ -296,18 +330,24 @@ def to_string(todos): return '\n'.join([serialize(todo) for todo in todos]) +# +# main class definition +# + 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 + :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 @@ -453,6 +493,10 @@ def __repr__(self): return self.__str__() +# +# search and format functions +# + def search(todos, text=None, completed=None, @@ -471,26 +515,35 @@ def search(todos, """ 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 @@ -516,7 +569,14 @@ def search(todos, 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 From 161b0313878cab0e7f00793e743ced160623b997 Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Tue, 16 Nov 2021 14:15:09 +0100 Subject: [PATCH 16/20] FIX: ignore single PLUS symbol for project specification (while reading from file) --- todotxtio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 036f661..9011090 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -37,8 +37,8 @@ '(?:(\d{4}-\d{2}-\d{2}) )?' \ ) -# project and subproject names -todo_project_regex = re.compile(' \+(\S*)') +# project and subproject names (ignore single "+" chars) +todo_project_regex = re.compile(' \+(\S+)') # context and subcontext names todo_context_regex = re.compile(' @(\S*)') From 87d9eaa622ee1b9b9a849c88623333d3383a099a Mon Sep 17 00:00:00 2001 From: "Kohn, Dr. Peter Nnamdi (TM-D)" Date: Tue, 16 Nov 2021 14:15:58 +0100 Subject: [PATCH 17/20] FIX: change order of member detection. now, e.g. projects are not read from descriptions. --- todotxtio.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index 9011090..94b3c8d 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -162,6 +162,29 @@ def from_string(string): + # + # evaluate remarks + # + + # detect and remove remarks from todotxt line at erliest possible time + # in order to not allow further token identification within the remarks + # but only the remaining text section. otherwise, the usage of tokens + # within the remarks would have the potential to mess up the task's + # metadata. + + # get all remark portions as a list of strings + todo_remarks = todo_remarks_regex.findall(text) + if todo_remarks: + # concatenate portions + todo_remarks = '\\'.join(todo_remarks) + # translate LINE BREAKS + todo.remarks = todo_remarks.replace('\\','\n') + # remove all remark portions from text + text = todo_remarks_regex.sub('', text).strip() + + + + # # evaluate contexts and projects # @@ -181,23 +204,6 @@ def from_string(string): - # - # evaluate remarks - # - - # get all remark portions as a list of strings - todo_remarks = todo_remarks_regex.findall(text) - if todo_remarks: - # concatenate portions - todo_remarks = '\\'.join(todo_remarks) - # translate LINE BREAKS - todo.remarks = todo_remarks.replace('\\','\n') - # remove all remark portions from text - text = todo_remarks_regex.sub('', text).strip() - - - - # # evaluate links # From 9a16193b580532c4bbb381e92775ef105a93afb8 Mon Sep 17 00:00:00 2001 From: nnako Date: Tue, 16 Nov 2021 14:16:44 +0100 Subject: [PATCH 18/20] todotxt-v1.1.2 --- todotxtio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index 94b3c8d..444cb1d 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -4,7 +4,7 @@ import re import io -__version__ = '1.1.1' +__version__ = '1.1.2' __all__ = [ 'from_dicts', From 68d2b208d0d777d01e4f78f225c37703e06adb6c Mon Sep 17 00:00:00 2001 From: nnako Date: Sun, 9 Jan 2022 07:46:22 +0100 Subject: [PATCH 19/20] FIX: stabilize project / subject identification against special characters instilled e.g. by GitLab special characters following the plus sign will skip the element --- todotxtio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todotxtio.py b/todotxtio.py index 444cb1d..bb4216d 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -38,7 +38,7 @@ ) # project and subproject names (ignore single "+" chars) -todo_project_regex = re.compile(' \+(\S+)') +todo_project_regex = re.compile(' \+(\w\S+)') # context and subcontext names todo_context_regex = re.compile(' @(\S*)') From fe5d5f20b0401d4318198b6ff61a1a36ebf4ddcf Mon Sep 17 00:00:00 2001 From: nnako Date: Mon, 10 Jan 2022 06:30:57 +0100 Subject: [PATCH 20/20] FIX: use recursive regular expressions for identifying remarks correctly --- todotxtio.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/todotxtio.py b/todotxtio.py index bb4216d..7b07b0c 100644 --- a/todotxtio.py +++ b/todotxtio.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import os -import re +import regex as re import io __version__ = '1.1.2' @@ -56,7 +56,8 @@ todo_filelink_regex = re.compile(' (http://|https://|link:)(\S*)') # text block of remarks -todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') +# todo_remarks_regex = re.compile(' \{([^\{\}]*)\}') # this one does not cover necessary recursions +todo_remarks_regex = re.compile('{((?:[^{}]|(?R))*)}') # all other information as tags todo_tag_regex = re.compile(' ([A-z]\S*?):(\S*)') @@ -172,6 +173,16 @@ def from_string(string): # within the remarks would have the potential to mess up the task's # metadata. + # TECH CONCEPT + # especially task management systems like GitLab insert their own + # special characters into the remarks section of tasks. e.g. when + # providing Markdown or other formats. this might lead to "nested" + # curly braces, which would be mistakenly identified as delimiters for + # remarks when evaluated. to solve this, a "recursive" regex mechanism + # is needed. so, the "re" library has been replaced by the "regex" + # library which has a compatible interface but provides recursive + # functionalities. + # get all remark portions as a list of strings todo_remarks = todo_remarks_regex.findall(text) if todo_remarks: