Commit 35cb964e authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into column-ui/master

Conflicts:
	setup.py
	topydo/lib/ExpressionCommand.py
parents d172148f 14b8d769
[run]
source = topydo
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
omit =
topydo/lib/ExitCommand.py
topydo/lib/Version.py
*.pyc
*.sw?
build
dist
install
.coverage
# Distribution / packaging
.Python
env/
env*/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Sublime Text
*.sublime-*
sudo: false # run on new infrastructure
language: python
python:
- "2.7"
......@@ -10,6 +11,10 @@ install:
- "pip install icalendar"
- "pip install pylint"
script: "./run-tests.sh"
# Cache Dependencies
cache:
directories:
- $HOME/travis/.cache/pip
notifications:
webhooks:
urls:
......
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 @mpcjanssen.
* 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
---
* Remove 'ical' subcommand in favor of 'topydo ls -f ical'
* Remove options highlight_projects_colors in favor of colorscheme options. In case you wish to disable the project/context colors, assign an empty value in the configuration file:
[colorscheme]
project_color =
context_color =
* `del`, `depri`, `do`, `pri`, `postpone` now support now expression like `ls`
does, using the `-e` flag (Jacek Sowiński, @mruwek).
* Fix `ls` when searching for a certain key:value where value is a string.
* Disable auto archive when the option archive_filename is empty.
* Add option auto_creation_date to enable/disable the creation date being added
to new todo items.
* Calculate relative dates correctly in long-running `prompt` sessions.
* `pri` also accepts priorities in the form (A), [A] or any other bracket.
* Add `listcontext` and `listcontexts` as aliases of `lscon`.
* Highlight tags when the value is one character long.
* Cleanups
0.4.1
-----
* Fix infinite loop when `keep_sorted` is enabled in the configuration.
* Depend on prompt-toolkit >= 0.39, which fixes the history functionality in
prompt mode (up/down keys).
0.4
---
......
If you're reading this, you may have interest in enhancing topydo. Thank you!
Please read the following guidelines to get your enhancement / bug fixes
smoothly into topydo:
smoothly into topydo.
### General
* This Github page defaults to the **stable** branch which is for **bug fixes
only**. If you would like to add a new feature, make sure to make a Pull
Request on the `master` branch.
* Use descriptive commit messages.
### Coding style
* Please try to adhere to the coding style dictated by `pylint` as much
possible. I won't be very picky about long lines, but please try to avoid
them.
* I strongly prefer simple and short functions, doing only one thing. I'll
request you to refactor functions with massive indentation or don't fit
otherwise on a screen.
### Testing
* Run tests with:
./run-tests.sh [python2|python3]
......@@ -21,13 +37,17 @@ smoothly into topydo:
ever again.
* Features: add testcases that checks various inputs and outputs of your
feature. Be creative in trying to break the feature you've just implemented.
* Use descriptive commit messages.
* Check the test coverage of your contributed code, in particular if you
touched code in the topydo.lib or topydo.command packages:
### Coding style
pip install coverage
coverage run setup.py test
coverage report
Or alternatively, for a more friendly output, run:
coverage html
Which will generate annotated files in the *htmlcov* folder. The new code
should be marked green (i.e. covered).
* Please try to adhere to the coding style dictated by `pylint` as much
possible. I won't be very picky about long lines, but please try to avoid
them.
* I strongly prefer simple and short functions, doing only one thing. I'll
request you to refactor functions with massive indentation or don't fit
otherwise on a screen.
topydo
======
[![Build Status](https://travis-ci.org/bram85/topydo.svg?branch=master)](https://travis-ci.org/bram85/topydo) [![Join the chat at https://gitter.im/bram85/topydo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bram85/topydo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/bram85/topydo.svg?branch=master)](https://travis-ci.org/bram85/topydo) [![Join the chat at https://gitter.im/bram85/topydo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bram85/topydo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=bram85&url=https://github.com/bram85/topydo&title=topydo&language=&tags=github&category=software)
topydo is a todo list application using the [todo.txt format][1]. It is heavily
inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a
......@@ -22,10 +22,17 @@ use topydo.
Install
-------
Install simply with:
Simply install with:
pip install topydo
### Optional dependencies
* icalendar : To print your todo.txt file as an iCalendar file
(not supported for Python 3.2).
* prompt-toolkit : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion.
Demo
----
......
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.4",
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'],
'urwid': ['urwid >= 1.3.0'],
'edit-cmd-tests': ['mock'],
},
......@@ -21,7 +47,7 @@ setup(
'console_scripts': ['topydo = topydo.cli.UILoader:main'],
},
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
......
......@@ -266,6 +266,16 @@ class AddCommandTest(CommandTest):
self.assertEqual(self.output, u("| 1| {tod} Foo @fo\u00f3b\u0105r due:{tod} id:1\n| 2| {tod} Bar +baz t:{tod} p:1\n".format(tod=self.today)))
self.assertEqual(self.errors, "")
def test_add_task_without_date(self):
config(p_overrides={('add', 'auto_creation_date'): '0'})
args = ["New todo"]
command = AddCommand.AddCommand(args, self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.todolist.todo(1).source(), "New todo")
self.assertEqual(self.errors, "")
def test_help(self):
command = AddCommand.AddCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -35,6 +35,8 @@ class DeleteCommandTest(CommandTest):
todos = [
"Foo id:1",
"Bar p:1",
"a @test with due:2015-06-03",
"a @test with +project",
]
self.todolist = TodoList(todos)
......@@ -62,7 +64,7 @@ class DeleteCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 0)
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Bar\nRemoved: Foo\n")
self.assertEqual(self.errors, "")
......@@ -71,7 +73,7 @@ class DeleteCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1) # force won't delete subtasks
self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "")
......@@ -80,7 +82,7 @@ class DeleteCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1) # force won't delete subtasks
self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "")
......@@ -116,7 +118,9 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["8to"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.todolist.print_todos(), "Foo")
result = "Foo\na @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.print_todos(), result)
self.assertRaises(InvalidTodoException, self.todolist.todo, 'b0n')
def test_multi_del1(self):
......@@ -124,14 +128,20 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertEqual(self.todolist.count(), 0)
result = "a @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.todolist.print_todos(), result)
def test_multi_del2(self):
""" Test deletion of multiple items. """
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertEqual(self.todolist.count(), 0)
result = "a @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.todolist.print_todos(), result)
def test_multi_del3(self):
""" Fail if any of supplied todo numbers is invalid. """
......@@ -160,6 +170,50 @@ class DeleteCommandTest(CommandTest):
self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_del1(self):
command = DeleteCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
result = "Removed: a @test with due:2015-06-03\nRemoved: a @test with +project\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_del2(self):
command = DeleteCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Removed: a @test with due:2015-06-03\n")
self.assertEqual(self.errors, "")
def test_expr_del3(self):
command = DeleteCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_del4(self):
""" Remove only relevant todo items. """
command = DeleteCommand(["-e", ""], self.todolist, self.out, self.error, None)
command.execute()
result = "Foo"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1)
self.assertEqual(self.todolist.print_todos(), result)
def test_expr_del5(self):
""" Force deleting unrelevant items with additional -x flag. """
command = DeleteCommand(["-xe", ""], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 0)
def test_empty(self):
command = DeleteCommand([], self.todolist, self.out, self.error)
command.execute()
......
......@@ -211,6 +211,14 @@ class DepCommandTest(CommandTest):
self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n")
def test_ls7(self):
command = DepCommand(["ls", "top", "99"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n")
def gc_helper(self, p_subcommand):
command = DepCommand([p_subcommand], self.todolist, self.out, self.error)
command.execute()
......
......@@ -28,6 +28,9 @@ class DepriCommandTest(CommandTest):
"(A) Foo",
"Bar",
"(B) Baz",
"(E) a @test with due:2015-06-03",
"(Z) a @test with +project p:1",
"(D) Bax id:1",
]
self.todolist = TodoList(todos)
......@@ -69,6 +72,50 @@ class DepriCommandTest(CommandTest):
self.assertEqual(self.output, "Priority removed.\n| 1| Foo\nPriority removed.\n| 3| Baz\n")
self.assertEqual(self.errors, "")
def test_expr_depri1(self):
command = DepriCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
result = "Priority removed.\n| 4| a @test with due:2015-06-03\nPriority removed.\n| 5| a @test with +project p:1\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_depri2(self):
command = DepriCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
result = "Priority removed.\n| 4| a @test with due:2015-06-03\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_depri3(self):
command = DepriCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_depri4(self):
""" Don't remove priority from unrelevant todo items. """
command = DepriCommand(["-e", "Bax"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_depri5(self):
""" Force unprioritizing unrelevant items with additional -x flag. """
command = DepriCommand(["-xe", "Bax"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Priority removed.\n| 6| Bax id:1\n")
self.assertEqual(self.errors, "")
def test_invalid1(self):
command = DepriCommand(["99"], self.todolist, self.out, self.error)
......
......@@ -41,6 +41,8 @@ class DoCommandTest(CommandTest):
"Subtodo of inactive p:2",
"Strict due:2014-01-01 rec:1d",
"Invalid rec:1",
"a @test with due:2015-06-03",
"a @test with +project",
]
self.todolist = TodoList(todos)
......@@ -123,7 +125,7 @@ class DoCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.count(), 10)
self.assertEqual(self.todolist.count(), 12)
def test_recurrence(self):
self.assertFalse(self.todolist.todo(4).has_tag('due'))
......@@ -131,7 +133,7 @@ class DoCommandTest(CommandTest):
self._recurrence_helper(["4"])
self.assertTrue(self.todolist.todo(4).is_completed())
result = "| 10| {today} Recurring! rec:1d due:{tomorrow}\nCompleted: x {today} Recurring! rec:1d\n".format(today=self.today, tomorrow=self.tomorrow)
result = "| 12| {today} Recurring! rec:1d due:{tomorrow}\nCompleted: x {today} Recurring! rec:1d\n".format(today=self.today, tomorrow=self.tomorrow)
self.assertEqual(self.output, result)
todo = self.todolist.todo(10)
......@@ -140,13 +142,13 @@ class DoCommandTest(CommandTest):
def test_strict_recurrence1(self):
self._recurrence_helper(["-s", "8"])
result = "| 10| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
result = "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
self.assertEqual(self.output, result)
def test_strict_recurrence2(self):
self._recurrence_helper(["--strict", "8"])
result = "| 10| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
result = "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
self.assertEqual(self.output, result)
def test_invalid1(self):
......@@ -254,7 +256,7 @@ class DoCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "")
def test_do_custom_date6(self):
......@@ -267,7 +269,7 @@ class DoCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "")
def test_do_custom_date7(self):
......@@ -279,7 +281,7 @@ class DoCommandTest(CommandTest):
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.output, "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "")
def test_multi_do1(self):
......@@ -320,10 +322,10 @@ class DoCommandTest(CommandTest):
"""
Check output when all supplied todo numbers are invalid.
"""
command = DoCommand(["99", "10"], self.todolist, self.out, self.error, _no_prompt)
command = DoCommand(["99", "15"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 10.\n")
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 15.\n")
def test_multi_do6(self):
""" Throw an error with invalid argument containing special characters. """
......@@ -333,6 +335,46 @@ class DoCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_do1(self):
command = DoCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Completed: x {t} a @test with due:2015-06-03\nCompleted: x {t} a @test with +project\n".format(t=self.today))
self.assertEqual(self.errors, "")
def test_expr_do2(self):
command = DoCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Completed: x {} a @test with due:2015-06-03\n".format(self.today))
self.assertEqual(self.errors, "")
def test_expr_do3(self):
command = DoCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_do4(self):
""" Don't do anything with unrelevant todo items. """
command = DoCommand(["-e", "Foo"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_do5(self):
""" Force marking unrelevant items as done with additional -x flag. """
command = DoCommand(["-xe", "Foo"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
result = "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {t} Bar p:1\nCompleted: x {t} Baz p:1\nCompleted: x {t} Foo id:1\n".format(t=self.today)
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_invalid_recurrence(self):
""" Show error message when an item has an invalid recurrence pattern. """
command = DoCommand(["9"], self.todolist, self.out, self.error, _no_prompt)
......
......@@ -139,7 +139,7 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.output, expected)
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog"))
@mock.patch('topydo.commands.EditCommand.call')
@mock.patch('topydo.commands.EditCommand.check_call')
def test_edit_archive(self, mock_call):
""" Edit archive file. """
mock_call.return_value = 0
......@@ -148,11 +148,29 @@ class EditCommandTest(CommandTest):
os.environ['EDITOR'] = editor
archive = config().archive()
command = EditCommand([u("-d")], self.todolist, self.out, self.error, None)
command = EditCommand(["-d"], self.todolist, self.out, self.error, None)
command.execute()
self.assertEqual(self.errors, "")
mock_call.assert_called_once_with([editor, archive])
@mock.patch('topydo.commands.EditCommand.check_call')
def test_edit_todotxt(self, mock_call):
""" Edit todo file. """
mock_call.return_value = 0
editor = 'vi'
os.environ['EDITOR'] = editor
todotxt = config().todotxt()
result = self.todolist.print_todos() # copy TodoList content *before* executing command
command = EditCommand([], self.todolist, self.out, self.error, None)
command.execute()
self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.print_todos(), result)
mock_call.assert_called_once_with([editor, todotxt])
if __name__ == '__main__':
unittest.main()
......@@ -307,7 +307,7 @@ class OrdinalTagFilterTest(TopydoTest):
self.todo1 = "Foo due:{}".format(self.today)
self.todo2 = "Bar due:{}".format(self.tomorrow)
self.todo3 = "Baz due:nonsense"
self.todo3 = "Baz due:Nonsense"
self.todo4 = "Fnord due:2014-10-32"
self.todos = [
......@@ -358,6 +358,22 @@ class OrdinalTagFilterTest(TopydoTest):
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo2)
def test_filter6(self):
otf = Filter.OrdinalTagFilter('due:non')
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo3)
def test_filter7(self):
otf = Filter.OrdinalTagFilter('due:Non')
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo3)
if __name__ == '__main__':
unittest.main()
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.lib.Todo import Todo
from test.TopydoTest import TopydoTest
class JsonPrinterTest(TopydoTest):
"""
Tests the functionality of printing a single todo item. Printing a list is
already covered by the ListCommand tests.
"""
def test_json(self):
""" Print a single todo item. """
printer = JsonPrinter()
todo = Todo('2015-06-06 Foo due:2015-05-32')
result = printer.print_todo(todo)
self.assertEqual(result, '{"completed": false, "completion_date": null, "contexts": [], "creation_date": "2015-06-06", "priority": null, "projects": [], "source": "2015-06-06 Foo due:2015-05-32", "tags": [["due", "2015-05-32"]], "text": "Foo"}')
......@@ -224,7 +224,6 @@ class ListCommandUnicodeTest(CommandTest):
self.assertEqual(self.output, expected)
class ListCommandJsonTest(CommandTest):
def test_json(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
......@@ -265,11 +264,14 @@ def replace_ical_tags(p_text):
IS_PYTHON_32 = (sys.version_info.major, sys.version_info.minor) == (3, 2)
class ListCommandIcalTest(CommandTest):
def setUp(self):
self.maxDiff = None
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
todolist = load_file_to_todolist("test/data/ListCommandIcalTest.txt")
command = ListCommand(["-f", "ical"], todolist, self.out, self.error)
command = ListCommand(["-x", "-f", "ical"], todolist, self.out, self.error)
command.execute()
self.assertTrue(todolist.is_dirty())
......
......@@ -37,6 +37,7 @@ class PostponeCommandTest(CommandTest):
"Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()),
"Past due:{}".format(self.past.isoformat()),
"Future due:{} t:{}".format(self.future.isoformat(), self.future_start.isoformat()),
"FutureStart t:{}".format(self.future.isoformat())
]
self.todolist = TodoList(todos)
......@@ -233,6 +234,55 @@ class PostponeCommandTest(CommandTest):
self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_postpone1(self):
command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist, self.out, self.error, None)
command.execute()
due = self.today + timedelta(14)
result = "| 2| Bar due:{d}\n| 3| Baz due:{d} t:{s}\n".format(d=due.isoformat(), s=self.start.isoformat())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_postpone2(self):
cmd_args = ["-e", "t:{}".format(self.start.isoformat()), "due:tod", "1w"]
command = PostponeCommand(cmd_args, self.todolist, self.out, self.error, None)
command.execute()
due = self.today + timedelta(7)
result = "| 3| Baz due:{} t:{}\n".format(due.isoformat(), self.start.isoformat())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_postpone3(self):
command = PostponeCommand(["-e", "@test", "due:tod", "+project", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_postpone4(self):
""" Don't postpone unrelevant todo items. """
command = PostponeCommand(["-e", "FutureStart", "1w"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_postpone5(self):
""" Force postponing unrelevant items with additional -x flag. """
command = PostponeCommand(["-xe", "FutureStart", "1w"], self.todolist, self.out, self.error, None)
command.execute()
due = self.today + timedelta(7)
result = "| 6| FutureStart t:{} due:{}\n".format(self.future.isoformat(), due.isoformat())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_help(self):
command = PostponeCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -27,6 +27,9 @@ class PriorityCommandTest(CommandTest):
todos = [
"(A) Foo",
"Bar",
"(B) a @test with due:2015-06-03",
"a @test with +project p:1",
"Baz id:1",
]
self.todolist = TodoList(todos)
......@@ -71,6 +74,58 @@ class PriorityCommandTest(CommandTest):
self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEqual(self.errors, "")
def test_set_prio6(self):
""" Allow priority to be set including parentheses. """
command = PriorityCommand(["Foo", "2", "(C)"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEqual(self.errors, "")
def test_expr_prio1(self):
command = PriorityCommand(["-e", "@test", "C"], self.todolist, self.out, self.error, None)
command.execute()
result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\nPriority set to C.\n| 4| (C) a @test with +project p:1\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_prio2(self):
command = PriorityCommand(["-e", "@test", "due:2015-06-03", "C"], self.todolist, self.out, self.error, None)
command.execute()
result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_prio3(self):
command = PriorityCommand(["-e", "@test", "due:2015-06-03", "+project", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_prio4(self):
""" Don't prioritize unrelevant todo items. """
command = PriorityCommand(["-e", "Baz", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_prio5(self):
""" Force prioritizing unrelevant items with additional -x flag. """
command = PriorityCommand(["-xe", "Baz", "D"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Priority set to D.\n| 5| (D) Baz id:1\n")
self.assertEqual(self.errors, "")
def test_invalid1(self):
command = PriorityCommand(["99", "A"], self.todolist, self.out, self.error)
command.execute()
......@@ -128,6 +183,29 @@ class PriorityCommandTest(CommandTest):
self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_invalid8(self):
"""
Test that there's only one capital surrounded by non-word
characters that makes up a priority.
"""
command = PriorityCommand(["2", "(Aa)"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n")
def test_invalid9(self):
"""
Test that there's only one capital surrounded by non-word
characters that makes up a priority.
"""
command = PriorityCommand(["2", "Aa"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n")
def test_empty(self):
command = PriorityCommand([], self.todolist, self.out, self.error)
command.execute()
......
......@@ -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)
......
......@@ -24,6 +24,7 @@ from topydo.lib.Todo import Todo
from topydo.lib.TodoFile import TodoFile
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.TodoList import TodoList
from topydo.lib.TodoListBase import TodoListBase
from test.TopydoTest import TopydoTest
class TodoListTester(TopydoTest):
......@@ -34,7 +35,7 @@ class TodoListTester(TopydoTest):
lines = [line for line in self.todofile.read() \
if re.search(r'\S', line)]
self.text = ''.join(lines)
self.todolist = TodoList(lines)
self.todolist = TodoListBase(lines)
def test_contexts(self):
self.assertEqual(set(['Context1', 'Context2']), \
......@@ -101,6 +102,16 @@ class TodoListTester(TopydoTest):
self.assertTrue(self.todolist.is_dirty())
self.assertRaises(InvalidTodoException, self.todolist.number, todo)
def test_delete2(self):
""" Try to remove a todo item that does not exist. """
count = self.todolist.count()
todo = Todo('Not in the list')
self.todolist.delete(todo)
self.assertEqual(self.todolist.count(), count)
self.assertFalse(self.todolist.is_dirty())
def test_append1(self):
todo = self.todolist.todo(3)
self.todolist.append(todo, "@Context3")
......@@ -137,13 +148,6 @@ class TodoListTester(TopydoTest):
""" Test that empty lines are not counted. """
self.assertEqual(self.todolist.count(), 5)
def test_todo_by_dep_id(self):
""" Tests that todos can be retrieved by their id tag. """
self.todolist.add("(C) Foo id:1")
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
def test_todo_number1(self):
todo = Todo("No number")
self.todolist.add_todo(todo)
......@@ -349,6 +353,14 @@ class TodoListDependencyTester(TopydoTest):
self.assertEqual(todo1.source(), 'Foo id:1')
self.assertEqual(todo2.source(), 'Bar p:1')
def test_todo_by_dep_id(self):
""" Tests that todos can be retrieved by their id tag. """
todolist = TodoList([])
todolist.add("(C) Foo id:1")
self.assertTrue(todolist.todo_by_dep_id('1'))
self.assertFalse(todolist.todo_by_dep_id('2'))
class TodoListCleanDependencyTester(TopydoTest):
def setUp(self):
super(TodoListCleanDependencyTester, self).setUp()
......
(C) Foo @Context2 Not@Context +Project1 Not+Project
(D) Bar @Context1 +Project2 p:1 due:2015-06-06
(C) Baz @Context1 +Project1 key:value id:1
(C) 2015-06-06 Drink beer @ home
(G) 13 + 29 = 42
(C) Only a start date t:2015-06-06
x 2015-06-06 A completed item due:2015-05-05
This diff was suppressed by a .gitattributes entry.
......@@ -7,6 +7,9 @@ default_command = ls
colors = 1
identifiers = linenumber ; or: text
[add]
auto_creation_date = 1
[ls]
hide_tags = id,p,ical
indent = 0
......
......@@ -36,6 +36,8 @@ _SUBCOMMAND_MAP = {
'ls': 'ListCommand',
'lscon': 'ListContextCommand',
'listcon': 'ListContextCommand',
'listcontext': 'ListContextCommand',
'listcontexts': 'ListContextCommand',
'lsprj': 'ListProjectCommand',
'lsproj': 'ListProjectCommand',
'listprj': 'ListProjectCommand',
......
......@@ -24,16 +24,17 @@ import sys
from six import PY2
from six.moves import input
MAIN_OPTS = "c:d:ht:v"
MAIN_OPTS = "ac:d:ht:v"
def usage():
""" Prints the command-line usage of topydo. """
print("""\
Synopsis: topydo [-c <config>] [-d <archive>] [-t <todo.txt>] subcommand [help|args]
Synopsis: topydo [-a] [-c <config>] [-d <archive>] [-t <todo.txt>] subcommand [help|args]
topydo -h
topydo -v
-a : Do not archive todo items on completion.
-c : Specify an alternative configuration file.
-d : Specify an alternative archive file (done.txt)
-h : This help text
......@@ -112,6 +113,7 @@ class CLIApplicationBase(object):
def __init__(self):
self.todolist = TodoList.TodoList([])
self.todofile = None
self.do_archive = True
def _usage(self):
usage()
......@@ -133,7 +135,9 @@ class CLIApplicationBase(object):
overrides = {}
for opt, value in opts:
if opt == "-c":
if opt == "-a":
self.do_archive = False
elif opt == "-c":
alt_config_path = value
elif opt == "-t":
overrides[('topydo', 'filename')] = value
......@@ -203,8 +207,12 @@ class CLIApplicationBase(object):
completed. It will do some maintenance and write out the final result
to the todo.txt file.
"""
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.todolist.is_dirty():
self._archive()
if self.do_archive and config().archive():
self._archive()
if config().keep_sorted():
self._execute(SortCommand, [])
......
......@@ -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. """
......
......@@ -47,7 +47,6 @@ class AddCommand(Command):
self.args = args
def get_todos_from_file(self):
if self.from_file == '-':
f = stdin
......@@ -107,7 +106,8 @@ class AddCommand(Command):
add_dependencies('before')
add_dependencies('after')
p_todo.set_creation_date(date.today())
if config().auto_creation_date():
p_todo.set_creation_date(date.today())
todo_text = _preprocess_input_todo(p_todo_text)
todo = self.todolist.add(todo_text)
......
......@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.commands.DCommand import DCommand
from topydo.lib.DCommand import DCommand
class DeleteCommand(DCommand):
def __init__(self, p_args, p_todolist,
......@@ -38,8 +38,16 @@ class DeleteCommand(DCommand):
self.execute_specific_core(p_todo)
def usage(self):
return """Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...]"""
return """\
Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...]
del [-x] -e <EXPRESSION>
"""
def help(self):
return """\
Deletes the todo item(s) with the given number(s) from the list."""
Deletes the todo item(s) with the given number(s) from the list.
It is also possible to delete items as complete with an expression using
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
"""
......@@ -25,23 +25,25 @@ class DepriCommand(MultiCommand):
super(DepriCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.get_todos(self.args)
def _execute_multi_specific(self):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
def execute_multi_specific(self):
try:
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos:
if todo.priority() != None:
self.todolist.set_priority(todo, None)
self.out("Priority removed.")
self.out(self.printer.print_todo(todo))
except IndexError:
self.error(self.usage())
for todo in self.todos:
if todo.priority() != None:
self.todolist.set_priority(todo, None)
self.out("Priority removed.")
self.out(self.printer.print_todo(todo))
def usage(self):
return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]"""
return """\
Synopsis: depri <NUMBER1> [<NUMBER2> ...]
depri [-x] -e <EXPRESSION>
"""
def help(self):
return """Removes the priority of the given todo item(s)."""
return """Removes the priority of the given todo item(s).
It is also possible to deprioritize items as complete with an expression using
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
"""
......@@ -16,10 +16,10 @@
from datetime import date
from topydo.commands.DCommand import DCommand
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):
......@@ -36,9 +36,13 @@ class DoCommand(DCommand):
def get_flags(self):
""" Additional flags. """
return ("d:s", ["date=", "strict"])
opts, long_opts = super(DoCommand, self).get_flags()
return ("d:s" + opts, ["date=", "strict"] + long_opts)
def process_flag(self, p_opt, p_value):
super(DoCommand, self).process_flag(p_opt, p_value)
if p_opt == "-s" or p_opt == "--strict":
self.strict_recurrence = True
elif p_opt == "-d" or p_opt == "--date":
......@@ -50,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)
......@@ -96,11 +99,18 @@ class DoCommand(DCommand):
self.todolist.set_todo_completed(p_todo, self.completion_date)
def usage(self):
return """Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...]"""
return """\
Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...]
do [-x] -e <EXPRESSION>
"""
def help(self):
return """Marks the todo(s) with given number(s) as complete.
It is also possible to mark todo items as complete with an expression using the
-e flag. Use -x to also process todo items that are normally invisible (with
the 'ls' subcommand).
In case a todo has subitems, a question is asked whether the subitems should be
marked as completed as well. When --force is given, no interaction is required
and the subitems are not marked completed.
......
......@@ -34,26 +34,25 @@ DEFAULT_EDITOR = 'vi'
# cannot use super() inside the class itself
BASE_TODOLIST = lambda tl: super(TodoList, tl)
class EditCommand(MultiCommand, ExpressionCommand):
class EditCommand(MultiCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(EditCommand, self).__init__(p_args, p_todolist, p_output,
p_error, p_input)
if len(self.args) == 0:
self.multi_mode = False
self.is_expression = False
self.edit_archive = False
self.last_argument = False
def _process_flags(self):
opts, args = self.getopt('xed')
for opt, value in opts:
if opt == '-d':
self.edit_archive = True
elif opt == '-x':
self.show_all = True
elif opt == '-e':
self.is_expression = True
def get_flags(self):
return ("d", [])
self.args = args
def process_flag(self, p_opt, p_value):
if p_opt == '-d':
self.edit_archive = True
self.multi_mode = False
def _todos_to_temp(self):
f = tempfile.NamedTemporaryFile()
......@@ -73,12 +72,20 @@ class EditCommand(MultiCommand, ExpressionCommand):
return todo_objs
def _open_in_editor(self, p_temp_file, p_editor):
def _open_in_editor(self, p_file):
try:
editor = os.environ['EDITOR'] or DEFAULT_EDITOR
except(KeyError):
editor = DEFAULT_EDITOR
try:
return check_call([p_editor, p_temp_file.name])
return check_call([editor, p_file])
except CalledProcessError:
self.error('Something went wrong in the editor...')
return 1
except(OSError):
self.error('There is no such editor as: ' + editor + '. '
'Check your $EDITOR and/or $PATH')
def _catch_todo_errors(self):
errors = []
......@@ -94,59 +101,35 @@ class EditCommand(MultiCommand, ExpressionCommand):
else:
return None
def execute(self):
if not super(EditCommand, self).execute():
return False
def _execute_multi_specific(self):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
try:
editor = os.environ['EDITOR'] or DEFAULT_EDITOR
except(KeyError):
editor = DEFAULT_EDITOR
try:
if len(self.args) < 1:
todo = config().todotxt()
temp_todos = self._todos_to_temp()
return call([editor, todo]) == 0
if not self._open_in_editor(temp_todos.name):
new_todos = self._todos_from_temp(temp_todos)
if len(new_todos) == len(self.todos):
for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo)
for todo in new_todos:
self.todolist.add_todo(todo)
self.out(self.printer.print_todo(todo))
else:
self._process_flags()
if self.edit_archive:
archive = config().archive()
return call([editor, archive]) == 0
if self.is_expression:
self.todos = self._view().todos
else:
self.get_todos(self.args)
todo_errors = self._catch_todo_errors()
if not todo_errors:
temp_todos = self._todos_to_temp()
if not self._open_in_editor(temp_todos, editor):
new_todos = self._todos_from_temp(temp_todos)
if len(new_todos) == len(self.todos):
for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo)
for todo in new_todos:
self.todolist.add_todo(todo)
self.out(self.printer.print_todo(todo))
else:
self.error('Number of edited todos is not equal to '
'number of supplied todo IDs.')
else:
self.error(self.usage())
else:
for error in todo_errors:
self.error(error)
except(OSError):
self.error('There is no such editor as: ' + editor + '. '
'Check your $EDITOR and/or $PATH')
self.error('Number of edited todos is not equal to '
'number of supplied todo IDs.')
else:
self.error(self.usage())
def _execute_not_multi(self):
if self.edit_archive:
archive = config().archive()
return self._open_in_editor(archive) == 0
else:
todo = config().todotxt()
return self._open_in_editor(todo) == 0
def usage(self):
return """Synopsis:
......
......@@ -46,7 +46,7 @@ class ListCommand(ExpressionCommand):
"""
try:
import icalendar as _
except ImportError:
except ImportError: # pragma: no cover
self.error("icalendar package is not installed.")
return False
......@@ -78,7 +78,7 @@ class ListCommand(ExpressionCommand):
Prints the todos in the right format.
Defaults to normal text output (with possible colors and other pretty
printing. If a format was specified on the commandline, this format is
printing). If a format was specified on the commandline, this format is
sent to the output.
"""
......@@ -101,7 +101,7 @@ class ListCommand(ExpressionCommand):
try:
self._process_flags()
except SyntaxError:
except SyntaxError: # pragma: no cover
# importing icalendar failed, most likely due to Python 3.2
self.error("icalendar is not supported in this Python version.")
return False
......
......@@ -17,7 +17,6 @@
from datetime import date, timedelta
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.Command import InvalidCommandArgument
from topydo.lib.Config import config
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date
......@@ -32,19 +31,16 @@ class PostponeCommand(MultiCommand):
p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False
self._process_flags()
self.get_todos(self.args[:-1])
self.last_argument = True
def _process_flags(self):
opts, args = self.getopt('s')
def get_flags(self):
return("s", [])
for opt, _ in opts:
if opt == '-s':
self.move_start_date = True
def process_flag(self, p_opt, p_value):
if p_opt == '-s':
self.move_start_date = True
self.args = args
def execute_multi_specific(self):
def _execute_multi_specific(self):
def _get_offset(p_todo):
offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat())
......@@ -55,34 +51,34 @@ class PostponeCommand(MultiCommand):
return offset_date
try:
pattern = self.args[-1]
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos:
offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset)
pattern = self.args[-1]
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
# pylint: disable=E1103
todo.set_tag(config().tag_start(), new_start.isoformat())
for todo in self.todos:
offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset)
if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
# pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat())
todo.set_tag(config().tag_start(), new_start.isoformat())
# pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty()
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid date pattern given.")
break
except (InvalidCommandArgument, IndexError):
self.error(self.usage())
self.todolist.set_dirty()
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid date pattern given.")
break
def usage(self):
return "Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
return """\
Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
postpone [-x] -e <EXPRESSION>
"""
def help(self):
return """\
......@@ -91,6 +87,10 @@ Postpone the todo item(s) with the given number(s) and the given pattern.
Postponing is done by adjusting the due date(s) of the todo(s), and if the -s flag is
given, the start date accordingly.
It is also possible to postpone items as complete with an expression using
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
The pattern is a relative date, written in the format <COUNT><PERIOD> where
count is a number and <PERIOD> is either 'd', 'w', 'm' or 'y', which stands for
days, weeks, months and years respectively. Example: 'postpone 1 1w' postpones
......
......@@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.Utils import is_valid_priority
......@@ -26,36 +28,42 @@ class PriorityCommand(MultiCommand):
super(PriorityCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.get_todos(self.args[:-1])
self.last_argument = True
def execute_multi_specific(self):
priority = None
def _execute_multi_specific(self):
def normalize_priority(p_priority):
match = re.search(r'\b([A-Z])\b', p_priority)
return match.group(1) if match else p_priority
try:
priority = self.args[-1]
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
priority = normalize_priority(self.args[-1])
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if is_valid_priority(priority):
for todo in self.todos:
old_priority = todo.priority()
self.todolist.set_priority(todo, priority)
if is_valid_priority(priority):
for todo in self.todos:
old_priority = todo.priority()
self.todolist.set_priority(todo, priority)
if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format(
old_priority, priority))
elif not old_priority:
self.out("Priority set to {}.".format(priority))
if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format(
old_priority, priority))
elif not old_priority:
self.out("Priority set to {}.".format(priority))
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid priority given.")
except IndexError:
self.error(self.usage())
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid priority given.")
def usage(self):
return """Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>"""
return """\
Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>
pri [-x] -e <EXPRESSION>
"""
def help(self):
return """\
Sets the priority of todo(s) the given number(s) to the given priority.
It is also possible to prioritize items as complete with an expression using
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
"""
......@@ -39,9 +39,7 @@ class SortCommand(Command):
sorted_todos = sorter.sort(self.todolist.todos())
self.todolist.erase()
for todo in sorted_todos:
self.todolist.add_todo(todo)
self.todolist.add_todos(sorted_todos)
def usage(self):
return """Synopsis: sort [expression]"""
......@@ -51,8 +49,13 @@ class SortCommand(Command):
Sorts the file according to the expression. If no expression is given, the
expression in the configuration is used.
The following sort properties are supported:
The expression is a comma separated list of attributes to sort on. The list is
evaluated in order, which means that the first attribute takes higher
precedence, then the second, etc.
The following sort attributes are supported:
* priority - Sort by priority
* creation - Sort by creation date
* completed - Sort by completion state
* importance - Sort by importance
......@@ -60,6 +63,10 @@ The following sort properties are supported:
* text - Sort by text
* <tag> - Sort by values of the given tag
Any property can be prefixed with 'asc:' and 'desc:' to specify the sort order.
The default is ascending sort.
Each item can optionally be prefixed with asc: and desc: to specify ascending
or descending sort respectively. If not specified, ascending sort is assumed.
Example:
desc:importance,due,desc:priority
"""
......@@ -84,8 +84,10 @@ class Command(object):
return result
def usage(self):
return "No usage text available for this command."
""" Returns a one-line synopsis for this command. """
raise NotImplementedError
def help(self):
return "No help text available for this command."
""" Returns the help text for this command. """
raise NotImplementedError
......@@ -38,7 +38,15 @@ class _Config:
(such as todo.txt location passed with -t). The key is a tuple of
(section, option), the value is the option's value.
"""
self.sections = ['topydo', 'tags', 'sort', 'ls', 'dep', 'colorscheme']
self.sections = [
'add',
'colorscheme',
'dep',
'ls',
'sort',
'tags',
'topydo',
]
self.defaults = {
# topydo
......@@ -48,6 +56,9 @@ class _Config:
'archive_filename' : 'done.txt',
'identifiers': 'linenumber',
# add
'auto_creation_date': '1',
# ls
'hide_tags': 'id,p,ical',
'indent': 0,
......@@ -185,6 +196,7 @@ class _Config:
def hidden_tags(self):
""" Returns a list of tags to be hidden from the 'ls' output. """
hidden_tags = self.cp.get('ls', 'hide_tags')
# pylint: disable=no-member
return [] if hidden_tags == '' else hidden_tags.split(',')
def priority_colors(self):
......@@ -233,6 +245,12 @@ class _Config:
except ValueError:
return int(self.defaults['link_color'])
def auto_creation_date(self):
try:
return self.cp.getboolean('add', 'auto_creation_date')
except ValueError:
return self.defaults['auto_creation_date'] == '1'
def config(p_path=None, p_overrides=None):
"""
Retrieve the config instance.
......
......@@ -35,29 +35,14 @@ class DCommand(MultiCommand):
self.force = False
self.process_flags()
self.length = len(self.todolist.todos()) # to determine newly activated todos
self.get_todos(self.args)
def get_flags(self):
""" Default implementation of getting specific flags. """
return ("", [])
return ("f", ["force"])
def process_flag(self, p_option, p_value):
""" Default implementation of processing specific flags. """
pass
def process_flags(self):
opts, args = self.get_flags()
opts, args = self.getopt("f" + opts, ["force"] + args)
for opt, value in opts:
if opt == "-f" or opt == "--force":
self.force = True
else:
self.process_flag(opt, value)
self.args = args
def process_flag(self, p_opt, p_value):
if p_opt == "-f" or p_opt == "--force":
self.force = True
def _uncompleted_children(self, p_todo):
return sorted(
......@@ -71,11 +56,10 @@ class DCommand(MultiCommand):
self.out(printer.print_list(p_todos))
def prompt_text(self):
return "Yes or no? [y/N] "
raise NotImplementedError
def prefix(self):
""" Prefix to use when printing a todo. """
return ""
raise NotImplementedError
def _process_subtasks(self, p_todo):
children = self._uncompleted_children(p_todo)
......@@ -116,19 +100,19 @@ class DCommand(MultiCommand):
return True
def condition_failed_text(self):
return ""
raise NotImplementedError
def execute_specific(self, _):
pass
raise NotImplementedError
def execute_specific_core(self, p_todo):
"""
The core operation on the todo itself. Also used to operate on
child/parent tasks.
"""
pass
raise NotImplementedError
def execute_multi_specific(self):
def _execute_multi_specific(self):
old_active = self._active_todos()
for todo in self.todos:
......
......@@ -35,6 +35,9 @@ class ExpressionCommand(Command):
self.sort_expression = config().sort_string()
self.show_all = False
# Commands using last argument differently (i.e as something other than
# todo ID/expression) have to set attribute below to True.
self.last_argument = False
def _filters(self):
filters = []
......@@ -43,7 +46,8 @@ class ExpressionCommand(Command):
filters.append(Filter.DependencyFilter(self.todolist))
filters.append(Filter.RelevanceFilter())
filters += Filter.get_filter_list(self.args)
args = self.args[:-1] if self.last_argument else self.args
filters += Filter.get_filter_list(args)
if not self.show_all:
filters.append(Filter.LimitFilter(config().list_limit()))
......
......@@ -29,8 +29,7 @@ class Filter(object):
return [t for t in p_todos if self.match(t)]
def match(self, _):
""" Default match value. """
return True
raise NotImplementedError
class NegationFilter(Filter):
def __init__(self, p_filter):
......@@ -157,13 +156,28 @@ ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):(?P<operator><=?|=|>=?|!)?(?P<value>\S+)"
class OrdinalTagFilter(Filter):
def __init__(self, p_expression):
super(OrdinalTagFilter, self).__init__()
match = re.match(ORDINAL_TAG_MATCH, p_expression)
self.expression = p_expression
match = re.match(ORDINAL_TAG_MATCH, self.expression)
if match:
self.key = match.group('key')
self.operator = match.group('operator') or '='
self.value = match.group('value')
def match(self, p_todo):
"""
Performs a match on a key:value tag in the todo.
First it tries to convert the value and the user-entered expression to
a date and makes a comparison if it succeeds, based on the given
operator (default ==).
Upon failure, it falls back to converting value and user-entered
expression to an integer and makes a numerical comparison based on the
given operator (default ==)
As a last resort, it falls back to using a Grep filter to see if the
user given expression is contained in the todo text.
"""
if not self.key or not p_todo.has_tag(self.key):
return False
......@@ -175,11 +189,15 @@ class OrdinalTagFilter(Filter):
operand2 = date_string_to_date(self.value)
except ValueError:
operand1 = p_todo.tag_value(self.key)
operand2 = self.value
try:
operand1 = int(p_todo.tag_value(self.key))
operand2 = int(self.value)
operand1 = int(operand1)
operand2 = int(operand2)
except ValueError:
return False
grep = GrepFilter(self.expression)
return grep.match(p_todo)
if self.operator == '<':
return operand1 < operand2
......
......@@ -69,15 +69,12 @@ class IcalPrinter(Printer):
try:
import icalendar
self.icalendar = icalendar
except (SyntaxError, ImportError):
except (SyntaxError, ImportError): # pragma: no cover
# icalendar does not support Python 3.2 resulting in a SyntaxError. Since
# this is an optional dependency, dropping Python 3.2 support altogether is
# too much. Therefore just disable the iCalendar functionality
self.icalendar = None
def print_todo(self, p_todo):
return self._convert_todo(p_todo).to_ical() if self.icalendar else ""
def print_list(self, p_todos):
result = ""
......
......@@ -16,10 +16,10 @@
from six import u
from topydo.lib.Command import Command
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.TodoListBase import InvalidTodoException
class MultiCommand(Command):
class MultiCommand(ExpressionCommand):
"""
A common class for operations that can work with multiple todo IDs.
"""
......@@ -33,14 +33,49 @@ class MultiCommand(Command):
self.todos = []
self.invalid_numbers = []
self.is_expression = False
self.multi_mode = True
def get_todos(self, p_numbers):
def get_flags(self):
""" Default implementation of getting specific flags. """
return ("", [])
def process_flag(self, p_option, p_value):
""" Default implementation of processing specific flags. """
pass
def _process_flags(self):
opts, long_opts = self.get_flags()
opts, args = self.getopt("xe" + opts, long_opts)
for opt, value in opts:
if opt == '-x':
self.show_all = True
elif opt == '-e':
self.is_expression = True
else:
self.process_flag(opt, value)
self.args = args
def get_todos_from_expr(self):
self.todos = self._view().todos
def get_todos(self):
""" Gets todo objects from supplied todo IDs """
for number in p_numbers:
try:
self.todos.append(self.todolist.todo(number))
except InvalidTodoException:
self.invalid_numbers.append(number)
if self.is_expression:
self.get_todos_from_expr()
else:
if self.last_argument:
numbers = self.args[:-1]
else:
numbers = self.args
for number in numbers:
try:
self.todos.append(self.todolist.todo(number))
except InvalidTodoException:
self.invalid_numbers.append(number)
def _catch_todo_errors(self):
"""
......@@ -65,23 +100,36 @@ class MultiCommand(Command):
else:
return None
def execute_multi_specific(self):
def _execute_multi_specific(self):
"""
Operations specific for particular command dealing with multiple todo
IDs.
"""
pass
def _execute_not_multi(self):
"""
Some commands can do something else besides operating on multiple todo
IDs. This method is a wrapper for those other operations.
"""
pass
def execute(self):
if not super(MultiCommand, self).execute():
return False
todo_errors = self._catch_todo_errors()
self._process_flags()
if not todo_errors:
self.execute_multi_specific()
if not self.multi_mode:
self._execute_not_multi()
else:
for error in todo_errors:
self.error(error)
self.get_todos()
todo_errors = self._catch_todo_errors()
if not todo_errors:
self._execute_multi_specific()
else:
for error in todo_errors:
self.error(error)
return True
......@@ -26,8 +26,7 @@ class Printer(object):
Subclasses must at least implement the print_todo method.
"""
def print_todo(self, p_todo):
""" Base implementation."""
return p_todo.source()
raise NotImplementedError
def print_list(self, p_todos):
"""
......
......@@ -30,8 +30,10 @@ class PrettyPrinterFilter(object):
"""
def filter(self, p_todo_str, _):
""" Default implementation returns an unmodified todo string. """
return p_todo_str
"""
Applies a filter to p_todo_str and returns a modified version of it.
"""
raise NotImplementedError
class PrettyPrinterColorFilter(PrettyPrinterFilter):
"""
......@@ -69,7 +71,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
p_todo_str)
# tags
p_todo_str = re.sub(r'\b\S+:[^/\s]\S+\b',
p_todo_str = re.sub(r'\b\S+:[^/\s]\S*\b',
metadata_color + r'\g<0>' + color,
p_todo_str)
......@@ -78,7 +80,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
' ' + link_color + r'\2\3' + color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR
p_todo_str += NEUTRAL_COLOR
return p_todo_str
......
......@@ -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,22 +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=date.today()):
return _advance_recurring_todo_helper(p_todo, p_offset)
def strict_advance_recurring_todo(p_todo, p_offset=date.today()):
"""
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
return _advance_recurring_todo_helper(p_todo, offset)
......@@ -35,13 +35,14 @@ def _add_months(p_sourcedate, p_months):
return date(year, month, day)
def _convert_pattern(p_length, p_periodunit, p_offset=date.today()):
def _convert_pattern(p_length, p_periodunit, p_offset=None):
"""
Converts a pattern in the form [0-9][dwmy] and returns a date from today
with the period of time added to it.
Converts a pattern in the form [0-9][dwmy] and returns a date from the
offset with the period of time added to it.
"""
result = None
p_offset = p_offset or date.today()
p_length = int(p_length)
if p_periodunit == 'd':
......@@ -80,7 +81,7 @@ def _convert_weekday_pattern(p_weekday):
shift = (target_day - day) % 7
return date.today() + timedelta(shift)
def relative_date_to_date(p_date, p_offset=date.today()):
def relative_date_to_date(p_date, p_offset=None):
"""
Transforms a relative date into a date object.
......@@ -93,6 +94,7 @@ def relative_date_to_date(p_date, p_offset=date.today()):
result = None
p_date = p_date.lower()
p_offset = p_offset or date.today()
relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmy])$', p_date, re.I)
......
......@@ -36,14 +36,11 @@ class TodoList(TodoListBase):
Should be given a list of strings, each element a single todo string.
The string will be parsed.
"""
self._todos = []
# initialize these first because the constructor calls add_list
self._tododict = {} # hash(todo) to todo lookup
self._depgraph = DirectedGraph()
self._todo_id_map = {}
self._id_todo_map = {}
self.add_list(p_todostrings)
self.dirty = False
super(TodoList, self).__init__(p_todostrings)
def todo_by_dep_id(self, p_dep_id):
"""
......
""" Version of Topydo. """
VERSION = '0.4'
VERSION = '0.6'
LICENSE = """Copyright (C) 2014 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