Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

FIX: compatibility with Python v2.x #9

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
deb9dc2
FIX: compatibility with Python v2.x
nnako Mar 29, 2018
b9d9e7a
FIX: the CREATION DATE will always appear in the 4th group of the def…
nnako Mar 29, 2018
5f5af12
removed some comments
nnako Mar 30, 2018
ec2f205
update of documentation and Python version info
nnako Mar 30, 2018
d87c529
NEW: PERSON specifications by using different SQUARE BRACKET formats
nnako Mar 31, 2018
71b6937
some minimal comment changes
Jul 24, 2019
f84d5bf
FIX: usage of serialize() instead of __str__() so in Python v2 unicod…
Jul 24, 2019
ac5c86f
FIX: facilitate normal time string "12:34" within text by forcing sta…
Jul 24, 2019
e46fc3a
FIX: occasional creation date mismatch corrected on import of TODOTXT…
Sep 2, 2019
dae5165
FIX: return REMARKS not as list but as string containing '\n'
Sep 3, 2019
c878bf7
FIX: remove warning message for REMARK to be no string
Sep 3, 2019
0bda3de
FIX: evaluate REMARKS before PERSONS to facilitate square brackets wi…
Sep 9, 2019
11080b6
NEW: prioritized evaluation of specific link attributes (http, https,
Nov 25, 2019
983b3a2
FIX: for detection of tags, take tag name until FIRST colon char
Apr 18, 2020
36cadce
added better comments
Dec 12, 2020
161b031
FIX: ignore single PLUS symbol for project specification (while readi…
Nov 16, 2021
87d9eaa
FIX: change order of member detection. now, e.g. projects are not rea…
Nov 16, 2021
9a16193
todotxt-v1.1.2
nnako Nov 16, 2021
68d2b20
FIX: stabilize project / subject identification against special chara…
nnako Jan 9, 2022
fe5d5f2
FIX: use recursive regular expressions for identifying remarks correctly
nnako Jan 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 79 additions & 26 deletions todotxtio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/python
Copy link
Owner

@EpocDotFr EpocDotFr Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file isn't meant to be executable. What is the purpose of this line?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... I always create my librarys so that in the end, they can be used as libraries AND can be executed as standalone tools (where this makes sense). So, you are right, here the line makes no sense, yet.

# -*- coding: utf-8 -*-

import os
import re
import io

__version__ = '0.2.2'

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry but the comments you added in this function doesn't add any useful information, given the statements are simple enough to understand. Please remove them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right. the comments don't add any functionality. It is just a question of style and readability in general (which is in the end a question of personal taste). I can remove it.

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'

Expand All @@ -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
Expand All @@ -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
Expand All @@ -144,28 +188,31 @@ 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])


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?
Expand Down Expand Up @@ -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
"""
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down