Commit 5b07c2a9 authored by root's avatar root

Merge branch 'master' of bram@bram85.nl:/home/bram/Projecten/topydo

parents a4cc9ce0 27cc49fe
[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
......@@ -3,3 +3,4 @@
build
dist
install
.coverage
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
......
......@@ -20,7 +20,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+)",
......
......@@ -29,7 +29,7 @@ from io import StringIO
from topydo.commands import AddCommand
from topydo.commands import ListCommand
from test.CommandTest import CommandTest, utf8
from test.CommandTest import CommandTest
from topydo.lib.Config import config
from topydo.lib import TodoList
......@@ -127,7 +127,7 @@ class AddCommandTest(CommandTest):
self.assertFalse(self.todolist.todo(1).has_tag("after"))
self.assertEqual(self.todolist.todo(1).source(), self.today + " Foo")
self.assertEqual(self.output, "| 1| " + str(self.todolist.todo(1)) + "\n")
self.assertEqual(self.output, "| 1| " + self.todolist.todo(1).source() + "\n")
self.assertEqual(self.errors, "")
def test_add_dep5(self):
......@@ -137,7 +137,7 @@ class AddCommandTest(CommandTest):
self.assertFalse(self.todolist.todo(1).has_tag("after"))
self.assertEqual(self.todolist.todo(1).source(), self.today + " Foo")
self.assertEqual(self.output, "| 1| " + str(self.todolist.todo(1)) + "\n")
self.assertEqual(self.output, "| 1| " + self.todolist.todo(1).source() + "\n")
self.assertEqual(self.errors, "")
def test_add_dep6(self):
......@@ -248,7 +248,7 @@ class AddCommandTest(CommandTest):
command = AddCommand.AddCommand([u("Special \u25c4")], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, utf8(u("| 1| {} Special \u25c4\n").format(self.today)))
self.assertEqual(self.output, u("| 1| {} Special \u25c4\n").format(self.today))
self.assertEqual(self.errors, "")
@mock.patch("topydo.commands.AddCommand.stdin", StringIO(u("Fo\u00f3 due:tod id:1\nB\u0105r before:1")))
......@@ -256,14 +256,14 @@ class AddCommandTest(CommandTest):
command = AddCommand.AddCommand(["-f", "-"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, utf8(u("| 1| {tod} Fo\u00f3 due:{tod} id:1\n| 2| {tod} B\u0105r p:1\n".format(tod=self.today))))
self.assertEqual(self.output, u("| 1| {tod} Fo\u00f3 due:{tod} id:1\n| 2| {tod} B\u0105r p:1\n".format(tod=self.today)))
self.assertEqual(self.errors, "")
def test_add_from_file(self):
command = AddCommand.AddCommand(["-f", "test/data/AddCommandTest-from_file.txt"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, utf8(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.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_help(self):
......
......@@ -31,8 +31,8 @@ class ArchiveCommandTest(CommandTest):
self.assertTrue(todolist.is_dirty())
self.assertTrue(archive.is_dirty())
self.assertEqual(str(todolist), "x Not complete\n(C) Active")
self.assertEqual(str(archive), "x 2014-10-19 Complete\nx 2014-10-20 Another one complete")
self.assertEqual(todolist.print_todos(), "x Not complete\n(C) Active")
self.assertEqual(archive.print_todos(), "x 2014-10-19 Complete\nx 2014-10-20 Another one complete")
if __name__ == '__main__':
unittest.main()
......
......@@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from six import PY2
from topydo.lib.Utils import escape_ansi
from test.TopydoTest import TopydoTest
......@@ -34,13 +33,5 @@ class CommandTest(TopydoTest):
if p_error:
self.errors += escape_ansi(p_error + "\n")
# utility for several commands
def utf8(p_string):
""" Converts a Unicode string to UTF-8 in case of Python 2. """
if PY2:
p_string = p_string.encode('utf-8')
return p_string
if __name__ == '__main__':
unittest.main()
......@@ -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(str(self.todolist), "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)
......
......@@ -17,7 +17,7 @@
import unittest
# We're searching for 'mock'
# pylint: disable=no-name-in-module
# pylint: disable=no-name-in-module
try:
from unittest import mock
except ImportError:
......@@ -27,7 +27,7 @@ from six import u
import os
from topydo.commands.EditCommand import EditCommand
from test.CommandTest import CommandTest, utf8
from test.CommandTest import CommandTest
from topydo.lib.TodoList import TodoList
from topydo.lib.Todo import Todo
from topydo.lib.Config import config
......@@ -44,7 +44,6 @@ class EditCommandTest(CommandTest):
self.todolist = TodoList(todos)
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit1(self, mock_open_in_editor):
""" Preserve dependencies after editing. """
......@@ -55,7 +54,7 @@ class EditCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
self.assertEqual(str(self.todolist), utf8(u("Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1")))
self.assertEqual(self.todolist.print_todos(), u("Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1"))
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
......@@ -69,7 +68,7 @@ class EditCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
self.assertEqual(str(self.todolist), utf8(u("Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat")))
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat"))
def test_edit3(self):
""" Throw an error after invalid todo number given as argument. """
......@@ -99,7 +98,7 @@ class EditCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, "Number of edited todos is not equal to number of supplied todo IDs.\n")
self.assertEqual(str(self.todolist), utf8(u("Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a")))
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a"))
def test_edit6(self):
""" Throw an error with invalid argument containing special characters. """
......@@ -121,7 +120,7 @@ class EditCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
self.assertEqual(str(self.todolist), utf8(u("Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat")))
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat"))
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
......@@ -133,14 +132,14 @@ class EditCommandTest(CommandTest):
command = EditCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
expected = utf8(u("| 3| Lazy Cat\n| 4| Lazy Dog\n"))
expected = u("| 3| Lazy Cat\n| 4| Lazy Dog\n")
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
self.assertEqual(self.output, expected)
self.assertEqual(str(self.todolist), utf8(u("Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog")))
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
......@@ -149,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()
......@@ -185,13 +185,6 @@ class FilterTest(TopydoTest):
self.assertEqual(todolist_to_string(filtered_todos), \
todolist_to_string(reference))
def test_filter19(self):
todos = load_file('test/data/FilterTest1.txt')
grep = Filter.GrepFilter(1)
filtered_todos = grep.filter(todos)
self.assertEqual(filtered_todos, [])
def test_filter20(self):
todos = load_file('test/data/FilterTest3.txt')
otf = Filter.OrdinalTagFilter('due:<2014-11-10')
......@@ -314,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 = [
......@@ -330,7 +323,7 @@ class OrdinalTagFilterTest(TopydoTest):
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(str(result[0]), self.todo1)
self.assertEqual(result[0].source(), self.todo1)
def test_filter2(self):
otf = Filter.OrdinalTagFilter('due:=today')
......@@ -338,7 +331,7 @@ class OrdinalTagFilterTest(TopydoTest):
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(str(result[0]), self.todo1)
self.assertEqual(result[0].source(), self.todo1)
def test_filter3(self):
otf = Filter.OrdinalTagFilter('due:>today')
......@@ -346,7 +339,7 @@ class OrdinalTagFilterTest(TopydoTest):
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(str(result[0]), self.todo2)
self.assertEqual(result[0].source(), self.todo2)
def test_filter4(self):
otf = Filter.OrdinalTagFilter('due:<1w')
......@@ -354,8 +347,8 @@ class OrdinalTagFilterTest(TopydoTest):
result = otf.filter(self.todos)
self.assertEqual(len(result), 2)
self.assertEqual(str(result[0]), self.todo1)
self.assertEqual(str(result[1]), self.todo2)
self.assertEqual(result[0].source(), self.todo1)
self.assertEqual(result[1].source(), self.todo2)
def test_filter5(self):
otf = Filter.OrdinalTagFilter('due:!today')
......@@ -363,7 +356,23 @@ class OrdinalTagFilterTest(TopydoTest):
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(str(result[0]), self.todo2)
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"}')
......@@ -22,7 +22,7 @@ import unittest
from topydo.lib.Config import config
from topydo.commands.ListCommand import ListCommand
from test.CommandTest import CommandTest, utf8
from test.CommandTest import CommandTest
from test.TestFacilities import load_file_to_todolist
class ListCommandTest(CommandTest):
......@@ -219,12 +219,11 @@ class ListCommandUnicodeTest(CommandTest):
self.assertFalse(self.todolist.is_dirty())
expected = utf8(u("| 1| (C) And some sp\u00e9cial tag:\u25c4\n"))
expected = u("| 1| (C) And some sp\u00e9cial tag:\u25c4\n")
self.assertEqual(self.output, expected)
class ListCommandJsonTest(CommandTest):
def test_json(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
......@@ -252,7 +251,7 @@ class ListCommandJsonTest(CommandTest):
with codecs.open('test/data/ListCommandUnicodeTest.json', 'r', encoding='utf-8') as json:
jsontext = json.read()
self.assertEqual(self.output, utf8(jsontext))
self.assertEqual(self.output, jsontext)
self.assertEqual(self.errors, "")
def replace_ical_tags(p_text):
......@@ -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())
......@@ -308,7 +310,7 @@ class ListCommandIcalTest(CommandTest):
with codecs.open('test/data/ListCommandUnicodeTest.ics', 'r', encoding='utf-8') as ical:
icaltext = ical.read()
self.assertEqual(replace_ical_tags(self.output), utf8(replace_ical_tags(icaltext)))
self.assertEqual(replace_ical_tags(self.output), replace_ical_tags(icaltext))
self.assertEqual(self.errors, "")
if __name__ == '__main__':
......
......@@ -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()
......
......@@ -31,13 +31,13 @@ class SortCommandTest(CommandTest):
command = SortCommand(["text"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(str(self.todolist), "First\n(A) Foo\n2014-06-14 Last")
self.assertEqual(self.todolist.print_todos(), "First\n(A) Foo\n2014-06-14 Last")
def test_sort2(self):
command = SortCommand([], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(str(self.todolist), "(A) Foo\n2014-06-14 Last\nFirst")
self.assertEqual(self.todolist.print_todos(), "(A) Foo\n2014-06-14 Last\nFirst")
def test_sort3(self):
""" Check that order does not influence the UID of a todo. """
......
......@@ -19,7 +19,7 @@ import unittest
from topydo.lib.Config import config
from topydo.lib.Sorter import Sorter
from test.TestFacilities import load_file, todolist_to_string, load_file_to_todolist
from test.TestFacilities import load_file, todolist_to_string, load_file_to_todolist, print_view
from test.TopydoTest import TopydoTest
class SorterTest(TopydoTest):
......@@ -128,7 +128,7 @@ class SorterTest(TopydoTest):
view = todolist.view(sorter, [])
result = load_file('test/data/SorterTest10-result.txt')
self.assertEqual(str(view), todolist_to_string(result))
self.assertEqual(print_view(view), todolist_to_string(result))
def test_sort15(self):
"""
......@@ -141,7 +141,7 @@ class SorterTest(TopydoTest):
view = todolist.view(sorter, [])
result = load_file('test/data/SorterTest11-result.txt')
self.assertEqual(str(view), todolist_to_string(result))
self.assertEqual(print_view(view), todolist_to_string(result))
def test_sort16(self):
"""
......@@ -153,7 +153,7 @@ class SorterTest(TopydoTest):
view = todolist.view(sorter, [])
result = load_file('test/data/SorterTest12-result.txt')
self.assertEqual(str(view), todolist_to_string(result))
self.assertEqual(print_view(view), todolist_to_string(result))
if __name__ == '__main__':
unittest.main()
......@@ -14,6 +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.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.Todo import Todo
from topydo.lib.TodoFile import TodoFile
from topydo.lib.TodoList import TodoList
......@@ -42,4 +43,8 @@ def load_file_to_todolist(p_filename):
def todolist_to_string(p_list):
""" Converts a todo list to a single string. """
return '\n'.join([str(t) for t in p_list])
return '\n'.join([t.source() for t in p_list])
def print_view(p_view):
printer = PrettyPrinter()
return printer.print_list(p_view.todos)
......@@ -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")
......@@ -133,22 +144,10 @@ class TodoListTester(TopydoTest):
self.assertRaises(InvalidTodoException, self.todolist.todo, count + 100)
self.assertFalse(self.todolist.is_dirty())
def test_string(self):
# readlines() always ends a string with \n, but join() in str(todolist)
# doesn't necessarily.
self.assertEqual(str(self.todolist) + '\n', self.text)
def test_count(self):
""" 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)
......@@ -354,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()
......
......@@ -18,7 +18,7 @@ import unittest
from topydo.lib import Filter
from topydo.lib.Sorter import Sorter
from test.TestFacilities import load_file, todolist_to_string
from test.TestFacilities import load_file, todolist_to_string, print_view
from topydo.lib.TodoFile import TodoFile
from topydo.lib.TodoList import TodoList
from test.TopydoTest import TopydoTest
......@@ -34,7 +34,7 @@ class ViewTest(TopydoTest):
todofilter = Filter.GrepFilter('+Project')
view = todolist.view(sorter, [todofilter])
self.assertEqual(str(view), todolist_to_string(ref))
self.assertEqual(print_view(view), todolist_to_string(ref))
if __name__ == '__main__':
unittest.main()
......
(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.
......@@ -55,6 +55,8 @@ class CLIApplication(CLIApplicationBase):
if self._execute(subcommand, args) == False:
sys.exit(1)
else:
self._post_execute()
def main():
""" Main entry point of the CLI. """
......
......@@ -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
......@@ -166,7 +170,7 @@ class CLIApplicationBase(object):
command.execute()
if archive.is_dirty():
archive_file.write(str(archive))
archive_file.write(archive.print_todos())
def _help(self, args):
if args == None:
......@@ -193,19 +197,27 @@ class CLIApplicationBase(object):
self._input())
if command.execute() != False:
self._post_execute()
return True
return False
def _post_execute(self):
"""
Should be called when executing the user requested command has been
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, [])
self.todofile.write(str(self.todolist))
self.todofile.write(self.todolist.print_todos())
def run(self):
raise NotImplementedError
......
......@@ -87,7 +87,7 @@ class TopydoCompleter(Completer):
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
def get_completions(self, p_document, p_complete_event):
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)
......
......@@ -27,7 +27,7 @@ def main():
args = sys.argv[1:]
try:
opts, args = getopt.getopt(args, MAIN_OPTS)
_, args = getopt.getopt(args, MAIN_OPTS)
except getopt.GetoptError as e:
error(str(e))
sys.exit(1)
......
......@@ -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,
......
......@@ -17,6 +17,7 @@
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.Config import config
from topydo.lib import Filter
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.Sorter import Sorter
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.View import View
......@@ -34,6 +35,8 @@ class DepCommand(Command):
except InvalidCommandArgument:
self.subsubcommand = None
self.printer = pretty_printer_factory(self.todolist)
def _handle_add(self):
(from_todo, to_todo) = self._get_todos()
......@@ -97,9 +100,8 @@ class DepCommand(Command):
if todos:
sorter = Sorter(config().sort_string())
instance_filter = Filter.InstanceFilter(todos)
view = View(sorter, [instance_filter], self.todolist,
self.printer)
self.out(view.pretty_print())
view = View(sorter, [instance_filter], self.todolist)
self.out(self.printer.print_list(view.todos))
except InvalidTodoException:
self.error("Invalid todo number given.")
except InvalidCommandArgument:
......
......@@ -25,20 +25,14 @@ 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):
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())
def _execute_multi_specific(self):
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))
def usage(self):
return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]"""
......
......@@ -16,7 +16,7 @@
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
......@@ -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":
......
......@@ -18,7 +18,7 @@ import os
from subprocess import call, check_call, CalledProcessError
import tempfile
from six import text_type, u
from six import u
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.MultiCommand import MultiCommand
......@@ -34,31 +34,30 @@ 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()
for todo in self.todos:
f.write((text_type(todo) + "\n").encode('utf-8'))
f.write((todo.source() + "\n").encode('utf-8'))
f.seek(0)
return f
......@@ -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()._viewdata
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:
......
......@@ -16,6 +16,7 @@
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.Config import config
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterIndentFilter,
PrettyPrinterHideTagFilter
......@@ -45,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
......@@ -81,10 +82,8 @@ class ListCommand(ExpressionCommand):
sent to the output.
"""
def _print_text():
"""
Outputs a pretty-printed text format of the todo list.
"""
if self.printer == None:
# create a standard printer with some filters
indent = config().list_indent()
hidden_tags = config().hidden_tags()
......@@ -92,14 +91,9 @@ class ListCommand(ExpressionCommand):
filters.append(PrettyPrinterIndentFilter(indent))
filters.append(PrettyPrinterHideTagFilter(hidden_tags))
self.out(self._view().pretty_print(filters))
self.printer = pretty_printer_factory(self.todolist, filters)
if self.printer == None:
_print_text()
else:
# we have set a special format, simply use the printer set in
# self.printer
self.out(str(self._view()))
self.out(self.printer.print_list(self._view().todos))
def execute(self):
if not super(ListCommand, self).execute():
......@@ -107,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,11 +17,9 @@
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
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import date_string_to_date
class PostponeCommand(MultiCommand):
......@@ -33,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())
......@@ -56,31 +51,28 @@ 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())
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())
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
def usage(self):
return "Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
......
......@@ -14,9 +14,10 @@
# 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.TodoListBase import InvalidTodoException
from topydo.lib.Utils import is_valid_priority
class PriorityCommand(MultiCommand):
......@@ -27,31 +28,30 @@ 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>"""
......
......@@ -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
......@@ -19,7 +19,6 @@ import re
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
class DCommand(MultiCommand):
"""
......@@ -36,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(
......@@ -72,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)
......@@ -110,26 +93,26 @@ class DCommand(MultiCommand):
return [todo for todo in self.todolist.todos()[:self.length]
if not self._uncompleted_children(todo) and todo.is_active()]
def condition(self, p_todo):
def condition(self, _):
"""
An additional condition whether execute_specific should be executed.
"""
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,13 +35,22 @@ 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 = []
def arg_filters():
result = []
for arg in self.args:
if self.last_argument:
args = self.args[:-1]
else:
args = self.args
for arg in args:
if re.match(Filter.ORDINAL_TAG_MATCH, arg):
argfilter = Filter.OrdinalTagFilter(arg)
elif len(arg) > 1 and arg[0] == '-':
......@@ -70,4 +79,4 @@ class ExpressionCommand(Command):
sorter = Sorter(self.sort_expression)
filters = self._filters()
return View(sorter, filters, self.todolist, self.printer)
return View(sorter, filters, self.todolist)
......@@ -15,9 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from six import text_type
from topydo.lib.Config import config
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.Utils import date_string_to_date
......@@ -30,9 +28,8 @@ class Filter(object):
return [t for t in p_todos if self.match(t)]
def match(self, p_todo):
""" Default match value. """
return True
def match(self, _):
raise NotImplementedError
class NegationFilter(Filter):
def __init__(self, p_filter):
......@@ -64,7 +61,7 @@ class GrepFilter(Filter):
super(GrepFilter, self).__init__()
# convert to string in case we receive integers
self.expression = text_type(p_expression)
self.expression = p_expression
if p_case_sensitive != None:
self.case_sensitive = p_case_sensitive
......@@ -159,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
......@@ -177,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
......@@ -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/>.
from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterColorFilter,
PrettyPrinterNumbers
)
class Printer(object):
"""
An abstract class that turns todo items into strings.
......@@ -21,8 +26,7 @@ class Printer(object):
Subclasses must at least implement the print_todo method.
"""
def print_todo(self, p_todo):
""" Base implementation. Simply returns the string conversion. """
return str(p_todo)
raise NotImplementedError
def print_list(self, p_todos):
"""
......@@ -57,9 +61,26 @@ class PrettyPrinter(Printer):
def print_todo(self, p_todo):
""" Given a todo item, pretty print it. """
todo_str = str(p_todo)
todo_str = p_todo.source()
for ppf in self.filters:
todo_str = ppf.filter(todo_str, p_todo)
return todo_str
def pretty_printer_factory(p_todolist, p_additional_filters=None):
""" Returns a pretty printer suitable for the ls and dep subcommands. """
p_additional_filters = p_additional_filters or []
printer = PrettyPrinter()
printer.add_filter(PrettyPrinterNumbers(p_todolist))
for ppf in p_additional_filters:
printer.add_filter(ppf)
# apply colors at the last step, the ANSI codes may confuse the
# preceding filters.
printer.add_filter(PrettyPrinterColorFilter())
return printer
......@@ -17,6 +17,7 @@
""" Provides filters used for pretty printing. """
import re
from six import u
from topydo.lib.Config import config
from topydo.lib.Colors import Colors, NEUTRAL_COLOR
......@@ -29,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):
"""
......@@ -99,7 +102,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter):
def filter(self, p_todo_str, p_todo):
""" Prepends the number to the todo string. """
return "|{:>3}| {}".format(self.todolist.number(p_todo), p_todo_str)
return u("|{:>3}| {}").format(self.todolist.number(p_todo), p_todo_str)
class PrettyPrinterHideTagFilter(PrettyPrinterFilter):
""" Removes all occurences of the given tags from the text. """
......
......@@ -58,10 +58,11 @@ def _advance_recurring_todo_helper(p_todo, p_offset):
return todo
def advance_recurring_todo(p_todo, p_offset=date.today()):
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=date.today()):
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.
......@@ -74,5 +75,5 @@ def strict_advance_recurring_todo(p_todo, p_offset=date.today()):
When no recurrence tag is present, an exception is raised.
"""
offset = p_todo.due_date() or p_offset
offset = p_todo.due_date() or p_offset or date.today()
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 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)
......
......@@ -20,12 +20,11 @@ This module contains the class that represents a single todo item.
from datetime import date
import re
from six import python_2_unicode_compatible, u
from six import u
from topydo.lib.TodoParser import parse_line
from topydo.lib.Utils import is_valid_priority
@python_2_unicode_compatible
class TodoBase(object):
"""
This class represents a single todo item in a todo.txt file. It maintains
......@@ -227,6 +226,3 @@ class TodoBase(object):
""" Returns the creation date of a todo. """
return self.fields['creationDate']
def __str__(self):
""" A printer for the todo item. """
return self.source()
......@@ -49,7 +49,7 @@ class TodoFile(object):
to the file.
"""
todofile = open(self.path, 'w')
todofile = codecs.open(self.path, 'w', encoding="utf-8")
if p_todos is list:
for todo in p_todos:
......
......@@ -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):
"""
......@@ -231,4 +228,3 @@ class TodoList(TodoListBase):
for todo in self._todos:
todo.attributes['parents'] = self.parents(todo)
......@@ -20,6 +20,7 @@ A list of todo items.
from datetime import date
import re
from six import text_type
from topydo.lib.Config import config
from topydo.lib import Filter
......@@ -124,7 +125,8 @@ class TodoListBase(object):
result = todo_by_linenumber(p_identifier)
if not result:
result = todo_by_regexp(p_identifier)
# convert integer to text so we pass on a valid regex
result = todo_by_regexp(text_type(p_identifier))
return result
......@@ -255,7 +257,10 @@ class TodoListBase(object):
self._todo_id_map[todo] = uid
self._id_todo_map[uid] = todo
def __str__(self):
def print_todos(self):
"""
Returns a pretty-printed string (without colors) of the todo items in
this list.
"""
printer = PrettyPrinter()
return printer.print_list(self._todos)
......@@ -16,62 +16,23 @@
""" A view is a list of todos, sorted and filtered. """
from six import python_2_unicode_compatible
from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterColorFilter,
PrettyPrinterNumbers
)
from topydo.lib.PrettyPrinter import PrettyPrinter
@python_2_unicode_compatible
class View(object):
"""
A view is instantiated by a todo list, usually obtained from a todo.txt
file. Also a sorter and a list of filters should be given that is applied
to the list.
A printer can be passed, but it won't be used when pretty_print() is
called, since it will instantiate its own pretty printer instance.
"""
def __init__(self, p_sorter, p_filters, p_todolist,
p_printer=PrettyPrinter()):
def __init__(self, p_sorter, p_filters, p_todolist):
self._todolist = p_todolist
self._viewdata = []
self._sorter = p_sorter
self._filters = p_filters
self._printer = p_printer
self.update()
def update(self):
"""
Updates the view data. Should be called when the backing todo list
has changed.
"""
self._viewdata = self._sorter.sort(self._todolist.todos())
@property
def todos(self):
""" Returns a sorted and filtered list of todos in this view. """
result = self._sorter.sort(self._todolist.todos())
for _filter in self._filters:
self._viewdata = _filter.filter(self._viewdata)
def pretty_print(self, p_pp_filters=None):
""" Pretty prints the view. """
p_pp_filters = p_pp_filters or []
# since we're using filters, always use PrettyPrinter
printer = PrettyPrinter()
printer.add_filter(PrettyPrinterNumbers(self._todolist))
for ppf in p_pp_filters:
printer.add_filter(ppf)
# apply colors at the last step, the ANSI codes may confuse the
# preceding filters.
printer.add_filter(PrettyPrinterColorFilter())
return printer.print_list(self._viewdata)
result = _filter.filter(result)
def __str__(self):
return self._printer.print_list(self._viewdata)
return result
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