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
---
......
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(
name = "topydo",
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.",
author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo",
install_requires = [
'six >= 1.9.0',
],
] + [p for p, cond in conditional_dependencies.items() if cond],
extras_require = {
'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit >= 0.39'],
'prompt-toolkit': ['prompt-toolkit >= 0.47'],
'edit-cmd-tests': ['mock'],
},
entry_points= {
......
......@@ -18,7 +18,7 @@ from datetime import date, timedelta
import unittest
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 test.TopydoTest import TopydoTest
......@@ -26,6 +26,7 @@ class RecurrenceTest(TopydoTest):
def setUp(self):
super(RecurrenceTest, self).setUp()
self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w")
def test_duedate1(self):
""" Where due date is in the future. """
......@@ -63,7 +64,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() - timedelta(1)
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)
......@@ -73,7 +74,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(8)
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)
......@@ -83,7 +84,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(7)
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)
......@@ -96,7 +97,7 @@ class RecurrenceTest(TopydoTest):
def test_noduedate2(self):
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.assertEqual(new_todo.due_date(), new_due)
......@@ -121,7 +122,7 @@ class RecurrenceTest(TopydoTest):
self.todo.set_tag(config().tag_start(), yesterday.isoformat())
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)
......@@ -135,6 +136,32 @@ class RecurrenceTest(TopydoTest):
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):
self.todo.remove_tag('rec')
self.assertRaises(NoRecurrenceException, advance_recurring_todo, self.todo)
......
......@@ -22,7 +22,7 @@ import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage
from topydo.cli.TopydoCompleter import TopydoCompleter
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
......@@ -83,7 +83,7 @@ class PromptApplication(CLIApplicationBase):
def run(self):
""" Main entry function. """
history = History()
history = InMemoryHistory()
while True:
# (re)load the todo.txt file (only if it has been modified)
......@@ -91,7 +91,8 @@ class PromptApplication(CLIApplicationBase):
try:
user_input = get_input(u'topydo> ', history=history,
completer=self.completer).split()
completer=self.completer,
complete_while_typing=False).split()
except (EOFError, KeyboardInterrupt):
sys.exit(0)
......
......@@ -14,6 +14,11 @@
# You should have received a copy of the GNU General Public License
# 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 re
......@@ -23,84 +28,95 @@ from topydo.lib.Config import config
from topydo.Commands import _SUBCOMMAND_MAP
from topydo.lib.RelativeDate import relative_date_to_date
def _date_suggestions():
"""
Returns a list of relative date that is presented to the user as auto
complete suggestions.
"""
# don't use strftime, prevent locales to kick in
days_of_week = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday"
}
dates = [
'today',
'tomorrow',
]
# 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
def _subcommands(p_word_before_cursor):
""" Generator for subcommand name completion. """
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 _dates(p_word_before_cursor):
""" Generator for date completion. """
def _date_suggestions():
"""
Returns a list of relative date that is presented to the user as auto
complete suggestions.
"""
# don't use strftime, prevent locales to kick in
days_of_week = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday"
}
dates = [
'today',
'tomorrow',
]
# 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):
"""
Completer class that completes projects, contexts, dates and
subcommands.
"""
def __init__(self, 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):
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:
yield Completion("+" + project, -len(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:
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, _):
# include all characters except whitespaces (for + and @)
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)
if is_first_word:
return self._subcommands(word_before_cursor)
return _subcommands(word_before_cursor)
elif word_before_cursor.startswith('+'):
return self._projects(word_before_cursor)
elif word_before_cursor.startswith('@'):
return self._contexts(word_before_cursor)
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() + ':'):
return self._dates(word_before_cursor)
return _dates(word_before_cursor)
return []
......@@ -20,6 +20,10 @@ import sys
import getopt
from topydo.cli.CLIApplicationBase import MAIN_OPTS, error
from topydo.cli.CLI import CLIApplication
# enable color on windows CMD
if "win32" in sys.platform:
import colorama
colorama.init()
def main():
""" Main entry point of the CLI. """
......
......@@ -19,7 +19,7 @@ from datetime import date
from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
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
class DoCommand(DCommand):
......@@ -54,12 +54,11 @@ class DoCommand(DCommand):
def _handle_recurrence(self, p_todo):
if p_todo.has_tag('rec'):
try:
if self.strict_recurrence:
new_todo = strict_advance_recurring_todo(p_todo,
self.completion_date)
else:
new_todo = advance_recurring_todo(p_todo,
self.completion_date)
new_todo = advance_recurring_todo(
p_todo,
p_offset=self.completion_date,
p_strict=self.strict_recurrence
)
self.todolist.add_todo(new_todo)
......
......@@ -25,24 +25,36 @@ from topydo.lib.Todo import Todo
class NoRecurrenceException(Exception):
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
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.
"""
todo = Todo(p_todo.source())
pattern = todo.tag_value('rec')
if not pattern:
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()
new_due = relative_date_to_date(pattern, p_offset)
new_due = relative_date_to_date(pattern, offset)
if not new_due:
raise NoRecurrenceException()
......@@ -57,23 +69,3 @@ def _advance_recurring_todo_helper(p_todo, p_offset):
todo.set_creation_date(date.today())
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 = '0.5'
VERSION = '0.6'
LICENSE = """Copyright (C) 2014 - 2015 Bram Schoenmakers
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