Commit bb714d07 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into stable

parents 838335a2 eb8a1d1d
0.6
---
* Recurrence patterns can be prefixed with a `+` to indicate strict recurrence
(i.e. based on due date rather than completion date. This syntax is inspired
from the SimpleTask project by @mpcjansen.
* Colors now work on the Windows commandline (thanks to @MinchinWeb). Requires
colorama to be installed.
* Do not print spurious color codes when colors are disabled in the
configuration (thanks to @MinchinWeb).
* In prompt mode, restore old auto-completion behavior: press Tab for
completion (instead of complete while typing).
* Various other minor fixes (thanks to @MinchinWeb).
0.5 0.5
--- ---
......
from setuptools import setup, find_packages from setuptools import setup, find_packages
import os
import re
import codecs
import sys
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open
return codecs.open(os.path.join(here, *parts), 'r').read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^VERSION = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
conditional_dependencies = {
"colorama>=0.2.5": "win32" in sys.platform,
}
setup( setup(
name = "topydo", name = "topydo",
packages = find_packages(exclude=["test"]), packages = find_packages(exclude=["test"]),
version = "0.5", version = find_version('topydo', 'lib', 'Version.py'),
description = "A command-line todo list application using the todo.txt format.", description = "A command-line todo list application using the todo.txt format.",
author = "Bram Schoenmakers", author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl", author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo", url = "https://github.com/bram85/topydo",
install_requires = [ install_requires = [
'six >= 1.9.0', 'six >= 1.9.0',
], ] + [p for p, cond in conditional_dependencies.items() if cond],
extras_require = { extras_require = {
'ical': ['icalendar'], 'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit >= 0.39'], 'prompt-toolkit': ['prompt-toolkit >= 0.47'],
'edit-cmd-tests': ['mock'], 'edit-cmd-tests': ['mock'],
}, },
entry_points= { entry_points= {
......
...@@ -18,7 +18,7 @@ from datetime import date, timedelta ...@@ -18,7 +18,7 @@ from datetime import date, timedelta
import unittest import unittest
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Recurrence import advance_recurring_todo, strict_advance_recurring_todo, NoRecurrenceException from topydo.lib.Recurrence import advance_recurring_todo, NoRecurrenceException
from topydo.lib.Todo import Todo from topydo.lib.Todo import Todo
from test.TopydoTest import TopydoTest from test.TopydoTest import TopydoTest
...@@ -26,6 +26,7 @@ class RecurrenceTest(TopydoTest): ...@@ -26,6 +26,7 @@ class RecurrenceTest(TopydoTest):
def setUp(self): def setUp(self):
super(RecurrenceTest, self).setUp() super(RecurrenceTest, self).setUp()
self.todo = Todo("Test rec:1w") self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w")
def test_duedate1(self): def test_duedate1(self):
""" Where due date is in the future. """ """ Where due date is in the future. """
...@@ -63,7 +64,7 @@ class RecurrenceTest(TopydoTest): ...@@ -63,7 +64,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() - timedelta(1) new_due = date.today() - timedelta(1)
self.todo.set_tag(config().tag_due(), past.isoformat()) self.todo.set_tag(config().tag_due(), past.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -73,7 +74,7 @@ class RecurrenceTest(TopydoTest): ...@@ -73,7 +74,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(8) new_due = date.today() + timedelta(8)
self.todo.set_tag(config().tag_due(), future.isoformat()) self.todo.set_tag(config().tag_due(), future.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -83,7 +84,7 @@ class RecurrenceTest(TopydoTest): ...@@ -83,7 +84,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(7) new_due = date.today() + timedelta(7)
self.todo.set_tag(config().tag_due(), today.isoformat()) self.todo.set_tag(config().tag_due(), today.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -96,7 +97,7 @@ class RecurrenceTest(TopydoTest): ...@@ -96,7 +97,7 @@ class RecurrenceTest(TopydoTest):
def test_noduedate2(self): def test_noduedate2(self):
new_due = date.today() + timedelta(7) new_due = date.today() + timedelta(7)
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertTrue(new_todo.has_tag(config().tag_due())) self.assertTrue(new_todo.has_tag(config().tag_due()))
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -121,7 +122,7 @@ class RecurrenceTest(TopydoTest): ...@@ -121,7 +122,7 @@ class RecurrenceTest(TopydoTest):
self.todo.set_tag(config().tag_start(), yesterday.isoformat()) self.todo.set_tag(config().tag_start(), yesterday.isoformat())
new_start = date.today() + timedelta(5) new_start = date.today() + timedelta(5)
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.start_date(), new_start) self.assertEqual(new_todo.start_date(), new_start)
...@@ -135,6 +136,32 @@ class RecurrenceTest(TopydoTest): ...@@ -135,6 +136,32 @@ class RecurrenceTest(TopydoTest):
self.assertEqual(new_todo.start_date(), new_start) self.assertEqual(new_todo.start_date(), new_start)
def test_strict_recurrence1(self):
"""
Strict recurrence where due date is in the past, using + notation in
expression.
"""
past = date.today() - timedelta(8)
new_due = date.today() - timedelta(1)
self.stricttodo.set_tag(config().tag_due(), past.isoformat())
new_todo = advance_recurring_todo(self.stricttodo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due)
def test_strict_recurrence2(self):
"""
Strict recurrence where due date is in the future, using + notation in
expression.
"""
future = date.today() + timedelta(1)
new_due = date.today() + timedelta(8)
self.stricttodo.set_tag(config().tag_due(), future.isoformat())
new_todo = advance_recurring_todo(self.stricttodo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due)
def test_no_recurrence(self): def test_no_recurrence(self):
self.todo.remove_tag('rec') self.todo.remove_tag('rec')
self.assertRaises(NoRecurrenceException, advance_recurring_todo, self.todo) self.assertRaises(NoRecurrenceException, advance_recurring_todo, self.todo)
......
...@@ -22,7 +22,7 @@ import sys ...@@ -22,7 +22,7 @@ import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage
from topydo.cli.TopydoCompleter import TopydoCompleter from topydo.cli.TopydoCompleter import TopydoCompleter
from prompt_toolkit.shortcuts import get_input from prompt_toolkit.shortcuts import get_input
from prompt_toolkit.history import History from prompt_toolkit.history import InMemoryHistory
from topydo.lib.Config import config, ConfigError from topydo.lib.Config import config, ConfigError
...@@ -83,7 +83,7 @@ class PromptApplication(CLIApplicationBase): ...@@ -83,7 +83,7 @@ class PromptApplication(CLIApplicationBase):
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
history = History() history = InMemoryHistory()
while True: while True:
# (re)load the todo.txt file (only if it has been modified) # (re)load the todo.txt file (only if it has been modified)
...@@ -91,7 +91,8 @@ class PromptApplication(CLIApplicationBase): ...@@ -91,7 +91,8 @@ class PromptApplication(CLIApplicationBase):
try: try:
user_input = get_input(u'topydo> ', history=history, user_input = get_input(u'topydo> ', history=history,
completer=self.completer).split() completer=self.completer,
complete_while_typing=False).split()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
sys.exit(0) sys.exit(0)
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module provides a completer class that can be used by get_input provided
by the prompt toolkit.
"""
import datetime import datetime
import re import re
...@@ -23,84 +28,95 @@ from topydo.lib.Config import config ...@@ -23,84 +28,95 @@ from topydo.lib.Config import config
from topydo.Commands import _SUBCOMMAND_MAP from topydo.Commands import _SUBCOMMAND_MAP
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
def _date_suggestions(): def _subcommands(p_word_before_cursor):
""" """ Generator for subcommand name completion. """
Returns a list of relative date that is presented to the user as auto subcommands = [sc for sc in sorted(_SUBCOMMAND_MAP.keys()) if
complete suggestions. sc.startswith(p_word_before_cursor)]
""" for command in subcommands:
# don't use strftime, prevent locales to kick in yield Completion(command, -len(p_word_before_cursor))
days_of_week = {
0: "Monday", def _dates(p_word_before_cursor):
1: "Tuesday", """ Generator for date completion. """
2: "Wednesday", def _date_suggestions():
3: "Thursday", """
4: "Friday", Returns a list of relative date that is presented to the user as auto
5: "Saturday", complete suggestions.
6: "Sunday" """
} # don't use strftime, prevent locales to kick in
days_of_week = {
dates = [ 0: "Monday",
'today', 1: "Tuesday",
'tomorrow', 2: "Wednesday",
] 3: "Thursday",
4: "Friday",
# show days of week up to next week 5: "Saturday",
dow = datetime.date.today().weekday() 6: "Sunday"
for i in range(dow + 2 % 7, dow + 7): }
dates.append(days_of_week[i % 7])
dates = [
# and some more relative days starting from next week 'today',
dates += ["1w", "2w", "1m", "2m", "3m", "1y"] 'tomorrow',
]
return dates
# show days of week up to next week
dow = datetime.date.today().weekday()
for i in range(dow + 2 % 7, dow + 7):
dates.append(days_of_week[i % 7])
# and some more relative days starting from next week
dates += ["1w", "2w", "1m", "2m", "3m", "1y"]
return dates
to_absolute = lambda s: relative_date_to_date(s).isoformat()
start_value_pos = p_word_before_cursor.find(':') + 1
value = p_word_before_cursor[start_value_pos:]
for reldate in _date_suggestions():
if not reldate.startswith(value):
continue
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
class TopydoCompleter(Completer): class TopydoCompleter(Completer):
"""
Completer class that completes projects, contexts, dates and
subcommands.
"""
def __init__(self, p_todolist): def __init__(self, p_todolist):
self.todolist = p_todolist self.todolist = p_todolist
def _subcommands(self, p_word_before_cursor):
subcommands = [sc for sc in sorted(_SUBCOMMAND_MAP.keys()) if sc.startswith(p_word_before_cursor)]
for command in subcommands:
yield Completion(command, -len(p_word_before_cursor))
def _projects(self, p_word_before_cursor): def _projects(self, p_word_before_cursor):
projects = [p for p in self.todolist.projects() if p.startswith(p_word_before_cursor[1:])] """ Generator for project completion. """
projects = [p for p in self.todolist.projects() if
p.startswith(p_word_before_cursor[1:])]
for project in projects: for project in projects:
yield Completion("+" + project, -len(p_word_before_cursor)) yield Completion("+" + project, -len(p_word_before_cursor))
def _contexts(self, p_word_before_cursor): def _contexts(self, p_word_before_cursor):
contexts = [c for c in self.todolist.contexts() if c.startswith(p_word_before_cursor[1:])] """ Generator for context completion. """
contexts = [c for c in self.todolist.contexts() if
c.startswith(p_word_before_cursor[1:])]
for context in contexts: for context in contexts:
yield Completion("@" + context, -len(p_word_before_cursor)) yield Completion("@" + context, -len(p_word_before_cursor))
def _dates(self, p_word_before_cursor):
to_absolute = lambda s: relative_date_to_date(s).isoformat()
start_value_pos = p_word_before_cursor.find(':') + 1
value = p_word_before_cursor[start_value_pos:]
for reldate in _date_suggestions():
if not reldate.startswith(value):
continue
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
def get_completions(self, p_document, _): def get_completions(self, p_document, _):
# include all characters except whitespaces (for + and @) # include all characters except whitespaces (for + and @)
word_before_cursor = p_document.get_word_before_cursor(True) word_before_cursor = p_document.get_word_before_cursor(True)
is_first_word = not re.match(r'\s*\S+\s', p_document.current_line_before_cursor) is_first_word = not re.match(r'\s*\S+\s', p_document.current_line_before_cursor)
if is_first_word: if is_first_word:
return self._subcommands(word_before_cursor) return _subcommands(word_before_cursor)
elif word_before_cursor.startswith('+'): elif word_before_cursor.startswith('+'):
return self._projects(word_before_cursor) return self._projects(word_before_cursor)
elif word_before_cursor.startswith('@'): elif word_before_cursor.startswith('@'):
return self._contexts(word_before_cursor) return self._contexts(word_before_cursor)
elif word_before_cursor.startswith(config().tag_due() + ':'): elif word_before_cursor.startswith(config().tag_due() + ':'):
return self._dates(word_before_cursor) return _dates(word_before_cursor)
elif word_before_cursor.startswith(config().tag_start() + ':'): elif word_before_cursor.startswith(config().tag_start() + ':'):
return self._dates(word_before_cursor) return _dates(word_before_cursor)
return [] return []
...@@ -20,6 +20,10 @@ import sys ...@@ -20,6 +20,10 @@ import sys
import getopt import getopt
from topydo.cli.CLIApplicationBase import MAIN_OPTS, error from topydo.cli.CLIApplicationBase import MAIN_OPTS, error
from topydo.cli.CLI import CLIApplication from topydo.cli.CLI import CLIApplication
# enable color on windows CMD
if "win32" in sys.platform:
import colorama
colorama.init()
def main(): def main():
""" Main entry point of the CLI. """ """ Main entry point of the CLI. """
......
...@@ -19,7 +19,7 @@ from datetime import date ...@@ -19,7 +19,7 @@ from datetime import date
from topydo.lib.DCommand import DCommand from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import PrettyPrinter from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.Recurrence import advance_recurring_todo, strict_advance_recurring_todo, NoRecurrenceException from topydo.lib.Recurrence import advance_recurring_todo, NoRecurrenceException
from topydo.lib.Utils import date_string_to_date from topydo.lib.Utils import date_string_to_date
class DoCommand(DCommand): class DoCommand(DCommand):
...@@ -54,12 +54,11 @@ class DoCommand(DCommand): ...@@ -54,12 +54,11 @@ class DoCommand(DCommand):
def _handle_recurrence(self, p_todo): def _handle_recurrence(self, p_todo):
if p_todo.has_tag('rec'): if p_todo.has_tag('rec'):
try: try:
if self.strict_recurrence: new_todo = advance_recurring_todo(
new_todo = strict_advance_recurring_todo(p_todo, p_todo,
self.completion_date) p_offset=self.completion_date,
else: p_strict=self.strict_recurrence
new_todo = advance_recurring_todo(p_todo, )
self.completion_date)
self.todolist.add_todo(new_todo) self.todolist.add_todo(new_todo)
......
...@@ -25,24 +25,36 @@ from topydo.lib.Todo import Todo ...@@ -25,24 +25,36 @@ from topydo.lib.Todo import Todo
class NoRecurrenceException(Exception): class NoRecurrenceException(Exception):
pass pass
def _advance_recurring_todo_helper(p_todo, p_offset): def advance_recurring_todo(p_todo, p_offset=None, p_strict=False):
""" """
Given a Todo item, return a new instance of a Todo item with the dates Given a Todo item, return a new instance of a Todo item with the dates
shifted according to the recurrence rule. shifted according to the recurrence rule.
The new date is calculated from the given p_offset value. Strict means that the real due date is taken as a offset, not today or a
future date to determine the offset.
When the todo item has no due date, then the date is used passed by the
caller (defaulting to today).
When no recurrence tag is present, an exception is raised. When no recurrence tag is present, an exception is raised.
""" """
todo = Todo(p_todo.source()) todo = Todo(p_todo.source())
pattern = todo.tag_value('rec') pattern = todo.tag_value('rec')
if not pattern: if not pattern:
raise NoRecurrenceException() raise NoRecurrenceException()
elif pattern.startswith('+'):
p_strict = True
# strip off the +
pattern = pattern[1:]
if p_strict:
offset = p_todo.due_date() or p_offset or date.today()
else:
offset = p_offset or date.today()
length = todo.length() length = todo.length()
new_due = relative_date_to_date(pattern, p_offset) new_due = relative_date_to_date(pattern, offset)
if not new_due: if not new_due:
raise NoRecurrenceException() raise NoRecurrenceException()
...@@ -57,23 +69,3 @@ def _advance_recurring_todo_helper(p_todo, p_offset): ...@@ -57,23 +69,3 @@ def _advance_recurring_todo_helper(p_todo, p_offset):
todo.set_creation_date(date.today()) todo.set_creation_date(date.today())
return todo return todo
def advance_recurring_todo(p_todo, p_offset=None):
p_offset = p_offset or date.today()
return _advance_recurring_todo_helper(p_todo, p_offset)
def strict_advance_recurring_todo(p_todo, p_offset=None):
"""
Given a Todo item, return a new instance of a Todo item with the dates
shifted according to the recurrence rule.
Strict means that the real due date is taken as a offset, not today or a
future date to determine the offset.
When the todo item has no due date, then the date is used passed by the
caller (defaulting to today).
When no recurrence tag is present, an exception is raised.
"""
offset = p_todo.due_date() or p_offset or date.today()
return _advance_recurring_todo_helper(p_todo, offset)
""" Version of Topydo. """ """ Version of Topydo. """
VERSION = '0.5' VERSION = '0.6'
LICENSE = """Copyright (C) 2014 - 2015 Bram Schoenmakers LICENSE = """Copyright (C) 2014 - 2015 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment