Commit c584bac6 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into stable

Conflicts:
	test/DoCommandTest.py
	topydo/lib/DoCommand.py
parents 27ea752a 97897e20
*.pyc *.pyc
*.sw? *.sw?
build
dist
install
language: python
python:
- "2.7"
install:
- "pip install ."
- "pip install icalendar"
script: python setup.py test
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/b7a69031304c472294ac
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false
0.3
---
* `edit` subcommand accepts a list of numbers or an expression to select which
items to edit. (Jacek Sowiński)
* The commands `del`, `do`, `pri`, `depri` and `postpone` can operate on multiple
todo items at once. (Jacek Sowiński)
* A new `ical` subcommand that outputs in the iCalendar format.
* New configuration option: `append_parent_contexts`. Similar to
`append_parent_projects` where the parent's contexts are automatically added
to child todo items. (Jacek Sowiński)
* New configuration option: `hide_tags` to hide certain tags from the `ls`
output. Multiple tags can be specified separated by commas. By default, `p`,
`id` and `ical` are hidden.
* Properly complete todo items with invalid recurrence patterns (`rec` tag).
* Fix assignment of dependency IDs: in some cases two distinct todos get the
same dependency ID.
Big thanks to Jacek for his contributions in this release.
0.2 0.2
--- ---
* A new 'edit' subcommand to launch an editor with the configured todo.txt file. * A new `edit` subcommand to launch an editor with the configured todo.txt file.
* Introduced textual identifiers in addition to line numbers. * Introduced textual identifiers in addition to line numbers.
Line numbers are still the default, textual identifiers can be enabled with Line numbers are still the default, textual identifiers can be enabled with
the option 'identifiers = text' in the configuration file (see topydo.conf). the option `identifiers = text` in the configuration file (see topydo.conf).
The advantage of these identifiers is that they are less prone to changes when The advantage of these identifiers is that they are less prone to changes when
something changes in the todo.txt file. For example, identifiers are much more something changes in the todo.txt file. For example, identifiers are much more
likely to remain the same when completing a todo item (and archiving it). With likely to remain the same when completing a todo item (and archiving it). With
...@@ -15,7 +35,7 @@ ...@@ -15,7 +35,7 @@
Sowiński). Sowiński).
* Multiple items can be marked as complete or deleted at once. * Multiple items can be marked as complete or deleted at once.
* Added option to automatically add the projects of the parent todo item when * Added option to automatically add the projects of the parent todo item when
adding a child todo item. Enable append_parent_projects in topydo.conf. adding a child todo item. Enable `append_parent_projects` in topydo.conf.
* `topydo help` shows a list of available subcommands. Moreover, you can run * `topydo help` shows a list of available subcommands. Moreover, you can run
`topydo help <subcommand>` as well. `topydo help <subcommand>` as well.
* Let setuptools provide a `topydo` executable. * Let setuptools provide a `topydo` executable.
......
topydo 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)
topydo is a todo list application using the [todo.txt format][1]. It is heavily 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 inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a
merge between the todo.txt CLI and a [number of extensions][3] that I wrote merge between the todo.txt CLI and a [number of extensions][3] that I wrote
on top of the CLI, hereafter refered to as todo.txt-tools. These extensions on top of the CLI. These extensions are:
are:
* Set **due** and **start dates**; * Set **due** and **start dates**;
* Custom sorting; * Custom sorting;
* Dealing with tags; * Dealing with tags;
* Maintain **dependencies** between todo items; * Maintain **dependencies** between todo items;
* Allow todos to **recur**; * Allow todo items to **recur**;
* Some conveniences when adding new items (e.g. adding creation date and use * Some conveniences when adding new items (e.g. adding creation date and use
**relative dates**); **relative dates**);
......
...@@ -3,11 +3,15 @@ from setuptools import setup ...@@ -3,11 +3,15 @@ from setuptools import setup
setup( setup(
name = "topydo", name = "topydo",
packages = ["topydo", "topydo.lib", "topydo.cli"], packages = ["topydo", "topydo.lib", "topydo.cli"],
version = "0.2", version = "0.3",
description = "A command-line todo list application using the todo.txt format.", description = "A command-line todo list application using the todo.txt format.",
author = "Bram Schoenmakers", author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl", author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo", url = "https://github.com/bram85/topydo",
extras_require = {
'ical': ['icalendar'],
'edit-cmd-tests': ['mock'],
},
entry_points= { entry_points= {
'console_scripts': ['topydo = topydo.cli.Main:main'], 'console_scripts': ['topydo = topydo.cli.Main:main'],
}, },
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -190,7 +190,27 @@ class AddCommandTest(CommandTest.CommandTest): ...@@ -190,7 +190,27 @@ class AddCommandTest(CommandTest.CommandTest):
command = ListCommand.ListCommand(["Bar"], self.todolist, self.out, self.error) command = ListCommand.ListCommand(["Bar"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertEquals(self.output, "|5dh| {today} Bar p:1 +Project\n|5dh| {today} Bar p:1 +Project\n".format(today=self.today)) self.assertEquals(self.output, "|5dh| {today} Bar p:1 +Project\n|5dh| {today} Bar +Project\n".format(today=self.today))
def test_add_dep10(self):
"""
The text ID shown after adding and after an 'ls' must be equal."
By appending the parent's contexts, the textual ID may change.
"""
config("test/data/todolist-uid-contexts.conf")
# pass identitiy function to for writing output, we're not interested
# in this output
command = AddCommand.AddCommand(["Foo @Context"], self.todolist, lambda t: t, self.error)
command.execute()
command = AddCommand.AddCommand(["Bar before:x2k"], self.todolist, self.out, self.error)
command.execute()
command = ListCommand.ListCommand(["Bar"], self.todolist, self.out, self.error)
command.execute()
self.assertEquals(self.output, "|5dc| {today} Bar p:1 @Context\n|5dc| {today} Bar @Context\n".format(today=self.today))
def test_add_reldate1(self): def test_add_reldate1(self):
command = AddCommand.AddCommand(["Foo due:today"], self.todolist, self.out, self.error) command = AddCommand.AddCommand(["Foo due:today"], self.todolist, self.out, self.error)
......
...@@ -133,11 +133,22 @@ class DeleteCommandTest(CommandTest.CommandTest): ...@@ -133,11 +133,22 @@ class DeleteCommandTest(CommandTest.CommandTest):
self.assertEquals(self.todolist.count(), 0) self.assertEquals(self.todolist.count(), 0)
def test_multi_del3(self): def test_multi_del3(self):
""" Test deletion of multiple items. """ """ Fail if any of supplied todo numbers is invalid. """
command = DeleteCommand(["99", "2"], self.todolist, self.out, self.error, _yes_prompt) command = DeleteCommand(["99", "2"], self.todolist, self.out, self.error, _yes_prompt)
command.execute() command.execute()
self.assertEquals(self.todolist.count(), 1) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
def test_multi_del4(self):
""" Check output when all supplied todo numbers are invalid. """
command = DeleteCommand(["99", "A"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: A.\n")
def test_empty(self): def test_empty(self):
command = DeleteCommand([], self.todolist, self.out, self.error) command = DeleteCommand([], self.todolist, self.out, self.error)
......
...@@ -26,20 +26,21 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -26,20 +26,21 @@ class DepriCommandTest(CommandTest.CommandTest):
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
"(B) Baz",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
def test_set_prio1(self): def test_depri1(self):
command = DepriCommand(["1"], self.todolist, self.out, self.error) command = DepriCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None) self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.output, "Priority removed.\nFoo\n") self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio2(self): def test_depri2(self):
command = DepriCommand(["2"], self.todolist, self.out, self.error) command = DepriCommand(["2"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -48,15 +49,26 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -48,15 +49,26 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "") self.assertEquals(self.output, "")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio3(self): def test_depri3(self):
command = DepriCommand(["Foo"], self.todolist, self.out, self.error) command = DepriCommand(["Foo"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None) self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.output, "Priority removed.\nFoo\n") self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_depri4(self):
command = DepriCommand(["1","Baz"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.todolist.todo(3).priority(), None)
self.assertEquals(self.output, "Priority removed.\n| 1| Foo\nPriority removed.\n| 3| Baz\n")
self.assertEquals(self.errors, "")
def test_invalid1(self): def test_invalid1(self):
command = DepriCommand(["99"], self.todolist, self.out, self.error) command = DepriCommand(["99"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -65,6 +77,22 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -65,6 +77,22 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given.\n") self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_invalid2(self):
command = DepriCommand(["99", "1"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
def test_invalid3(self):
command = DepriCommand(["99", "FooBar"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: FooBar.\n")
def test_empty(self): def test_empty(self):
command = DepriCommand([], self.todolist, self.out, self.error) command = DepriCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -312,20 +312,17 @@ class DoCommandTest(CommandTest.CommandTest): ...@@ -312,20 +312,17 @@ class DoCommandTest(CommandTest.CommandTest):
command = DoCommand(["99", "3"], self.todolist, self.out, self.error, _no_prompt) command = DoCommand(["99", "3"], self.todolist, self.out, self.error, _no_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.todo(3).is_completed()) self.assertFalse(self.todolist.todo(3).is_completed())
self.assertEquals(self.output, "Completed: x {} Baz p:1\n".format(self.today)) self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_multi_do5(self): def test_multi_do5(self):
""" """
When a todo item was generated by a recurring todo item, make sure Check output when all supplied todo numbers are invalid.
it cannot be completed in the same invocation.
""" """
command = DoCommand(["4", "10"], self.todolist, self.out, self.error, _no_prompt) command = DoCommand(["99", "10"], self.todolist, self.out, self.error, _no_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.todo(4).is_completed()) self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 10.\n")
self.assertFalse(self.todolist.todo(10).is_completed())
def test_invalid_recurrence(self): def test_invalid_recurrence(self):
""" Show error message when an item has an invalid recurrence pattern. """ """ Show error message when an item has an invalid recurrence pattern. """
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 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/>.
import unittest
import mock
import CommandTest
from topydo.lib.EditCommand import EditCommand
from topydo.lib.TodoList import TodoList
from topydo.lib.Todo import Todo
class EditCommandTest(CommandTest.CommandTest):
def setUp(self):
super(EditCommandTest, self).setUp()
todos = [
"Foo id:1",
"Bar p:1 @test",
"Baz @test",
]
self.todolist = TodoList(todos)
@mock.patch('topydo.lib.EditCommand.EditCommand._open_in_editor')
def test_edit1(self, mock_open_in_editor):
""" Preserve dependencies after editing. """
mock_open_in_editor.return_value = 0
command = EditCommand(["1"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.errors, "")
self.assertEquals(str(self.todolist), "Bar p:1 @test\nBaz @test\nFoo id:1")
@mock.patch('topydo.lib.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.lib.EditCommand.EditCommand._open_in_editor')
def test_edit2(self, mock_open_in_editor, mock_todos_from_temp):
""" Edit some todo. """
mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Lazy Cat')]
command = EditCommand(["Bar"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.errors, "")
self.assertEquals(str(self.todolist), "Foo id:1\nBaz @test\nLazy Cat")
def test_edit3(self):
""" Throw an error after invalid todo number given as argument. """
command = EditCommand(["FooBar"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_edit4(self):
""" Throw an error with pointing invalid argument. """
command = EditCommand(["Bar","4"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.errors, "Invalid todo number given: 4.\n")
@mock.patch('topydo.lib.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.lib.EditCommand.EditCommand._open_in_editor')
def test_edit5(self, mock_open_in_editor, mock_todos_from_temp):
""" Don't let to delete todos acidentally while editing. """
mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Only one line')]
command = EditCommand(["1","Bar"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.errors, "Number of edited todos is not equal to number of supplied todo IDs.\n")
self.assertEquals(str(self.todolist), "Foo id:1\nBar p:1 @test\nBaz @test")
@mock.patch('topydo.lib.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.lib.EditCommand.EditCommand._open_in_editor')
def test_edit_expr(self, mock_open_in_editor, mock_todos_from_temp):
""" Edit todos matching expression. """
mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Lazy Cat'), Todo('Lazy Dog')]
command = EditCommand(["-e","@test"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.errors, "")
self.assertEquals(str(self.todolist), "Foo id:1\nLazy Cat\nLazy Dog")
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/>.
import re
import unittest
from topydo.lib.Config import config
import CommandTest
from topydo.lib.IcalCommand import IcalCommand
import TestFacilities
class IcalCommandTest(CommandTest.CommandTest):
def setUp(self):
super(IcalCommandTest, self).setUp()
self.todolist = TestFacilities.load_file_to_todolist("test/data/ListCommandTest.txt")
def test_ical(self):
def replace_ical_tags(p_text):
# replace identifiers with dots, since they're random.
result = re.sub(r'\bical:....\b', 'ical:....', p_text)
result = re.sub(r'\bUID:....\b', 'UID:....', result)
return result
command = IcalCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
icaltext = ""
with open('test/data/ListCommandTest.ics', 'r') as ical:
icaltext = "".join(ical.readlines())
self.assertEquals(replace_ical_tags(self.output), replace_ical_tags(icaltext))
self.assertEquals(self.errors, "")
def test_help(self):
command = IcalCommand(["help"], self.todolist, self.out, self.error)
command.execute()
self.assertEquals(self.output, "")
self.assertEquals(self.errors, command.usage() + "\n\n" + command.help() + "\n")
if __name__ == '__main__':
unittest.main()
...@@ -26,16 +26,12 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -26,16 +26,12 @@ class ListCommandTest(CommandTest.CommandTest):
super(ListCommandTest, self).setUp() super(ListCommandTest, self).setUp()
self.todolist = TestFacilities.load_file_to_todolist("test/data/ListCommandTest.txt") self.todolist = TestFacilities.load_file_to_todolist("test/data/ListCommandTest.txt")
def tearDown(self):
# restore to the default configuration in case a custom one was set
config("")
def test_list1(self): def test_list1(self):
command = ListCommand([""], self.todolist, self.out, self.error) command = ListCommand([""], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list3(self): def test_list3(self):
...@@ -43,7 +39,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -43,7 +39,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list4(self): def test_list4(self):
...@@ -51,7 +47,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -51,7 +47,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list5(self): def test_list5(self):
...@@ -59,7 +55,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -59,7 +55,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2 p:1\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list6(self): def test_list6(self):
...@@ -75,7 +71,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -75,7 +71,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n") self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list8(self): def test_list8(self):
...@@ -83,7 +79,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -83,7 +79,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list9(self): def test_list9(self):
...@@ -91,7 +87,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -91,7 +87,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list10(self): def test_list10(self):
...@@ -99,7 +95,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -99,7 +95,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list11(self): def test_list11(self):
...@@ -119,7 +115,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -119,7 +115,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list13(self): def test_list13(self):
...@@ -127,7 +123,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -127,7 +123,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEquals(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list14(self): def test_list14(self):
...@@ -137,7 +133,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -137,7 +133,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, " | 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n | 4| (C) Drink beer @ home\n | 5| (C) 13 + 29 = 42\n | 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, " | 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n | 4| (C) Drink beer @ home\n | 5| (C) 13 + 29 = 42\n | 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list15(self): def test_list15(self):
...@@ -145,7 +141,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -145,7 +141,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list16(self): def test_list16(self):
...@@ -155,7 +151,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -155,7 +151,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "|6iu| (C) Foo @Context2 Not@Context +Project1 Not+Project\n|til| (C) Drink beer @ home\n| c5| (C) 13 + 29 = 42\n|xvb| (D) Bar @Context1 +Project2 p:1\n") self.assertEquals(self.output, "|6iu| (C) Foo @Context2 Not@Context +Project1 Not+Project\n|til| (C) Drink beer @ home\n| c5| (C) 13 + 29 = 42\n|xvb| (D) Bar @Context1 +Project2\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list17(self): def test_list17(self):
...@@ -163,7 +159,7 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -163,7 +159,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value id:1\n") self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_list18(self): def test_list18(self):
...@@ -173,6 +169,17 @@ class ListCommandTest(CommandTest.CommandTest): ...@@ -173,6 +169,17 @@ class ListCommandTest(CommandTest.CommandTest):
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEquals(self.output, "| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
def test_list19(self):
""" Force showing all tags. """
config('test/data/listcommand-tags.conf')
command = ListCommand(["-s", "text", "-x", "Project1"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEquals(self.errors, "")
def test_help(self): def test_help(self):
command = ListCommand(["help"], self.todolist, self.out, self.error) command = ListCommand(["help"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -164,6 +164,61 @@ class PostponeCommandTest(CommandTest.CommandTest): ...@@ -164,6 +164,61 @@ class PostponeCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "| 1| Foo due:{}\n".format(due.isoformat())) self.assertEquals(self.output, "| 1| Foo due:{}\n".format(due.isoformat()))
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_postpone14(self):
command = PostponeCommand(["1", "2", "1w"], self.todolist, self.out, self.error)
command.execute()
due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat()))
self.assertEquals(self.errors, "")
def test_postpone15(self):
command = PostponeCommand(["Foo", "2", "1w"], self.todolist, self.out, self.error)
command.execute()
due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat()))
self.assertEquals(self.errors, "")
def test_postpone16(self):
command = PostponeCommand(["-s", "2", "3", "1w"], self.todolist, self.out, self.error)
command.execute()
due = self.today + timedelta(7)
start = self.start + timedelta(7)
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "| 2| Bar due:{}\n| 3| Baz due:{} t:{}\n".format(due.isoformat(), due.isoformat(), start.isoformat()))
self.assertEquals(self.errors, "")
def test_postpone17(self):
command = PostponeCommand(["1", "2", "3"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "Invalid date pattern given.\n")
def test_postpone18(self):
command = PostponeCommand(["1", "99", "123", "1w"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 123.\n")
def test_postpone19(self):
command = PostponeCommand(["Zoo", "99", "123", "1w"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "Invalid todo number given: Zoo.\nInvalid todo number given: 99.\nInvalid todo number given: 123.\n")
def test_help(self): def test_help(self):
command = PostponeCommand(["help"], self.todolist, self.out, self.error) command = PostponeCommand(["help"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -35,7 +35,7 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -35,7 +35,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "Priority changed from A to B\n(B) Foo\n") self.assertEquals(self.output, "Priority changed from A to B\n| 1| (B) Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio2(self): def test_set_prio2(self):
...@@ -43,7 +43,7 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -43,7 +43,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "Priority set to Z.\n(Z) Bar\n") self.assertEquals(self.output, "Priority set to Z.\n| 2| (Z) Bar\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio3(self): def test_set_prio3(self):
...@@ -51,7 +51,7 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -51,7 +51,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "Priority changed from A to B\n(B) Foo\n") self.assertEquals(self.output, "Priority changed from A to B\n| 1| (B) Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio4(self): def test_set_prio4(self):
...@@ -59,7 +59,15 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -59,7 +59,15 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEquals(self.output, "(A) Foo\n") self.assertEquals(self.output, "| 1| (A) Foo\n")
self.assertEquals(self.errors, "")
def test_set_prio5(self):
command = PriorityCommand(["Foo", "2", "C"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_invalid1(self): def test_invalid1(self):
...@@ -71,6 +79,22 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -71,6 +79,22 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertEquals(self.errors, "Invalid todo number given.\n") self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_invalid2(self): def test_invalid2(self):
command = PriorityCommand(["1", "99", "A"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
def test_invalid3(self):
command = PriorityCommand(["98", "99", "A"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 98.\nInvalid todo number given: 99.\n")
def test_invalid4(self):
command = PriorityCommand(["1", "ZZ"], self.todolist, self.out, self.error) command = PriorityCommand(["1", "ZZ"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -78,7 +102,7 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -78,7 +102,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid priority given.\n") self.assertEquals(self.errors, "Invalid priority given.\n")
def test_invalid3(self): def test_invalid5(self):
command = PriorityCommand(["A"], self.todolist, self.out, self.error) command = PriorityCommand(["A"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -86,7 +110,7 @@ class PriorityCommandTest(CommandTest.CommandTest): ...@@ -86,7 +110,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEquals(self.errors, command.usage() + "\n") self.assertEquals(self.errors, command.usage() + "\n")
def test_invalid4(self): def test_invalid6(self):
command = PriorityCommand(["1"], self.todolist, self.out, self.error) command = PriorityCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -26,10 +26,6 @@ class SortCommandTest(CommandTest.CommandTest): ...@@ -26,10 +26,6 @@ class SortCommandTest(CommandTest.CommandTest):
super(SortCommandTest, self).setUp() super(SortCommandTest, self).setUp()
self.todolist = TestFacilities.load_file_to_todolist("test/data/SorterTest1.txt") self.todolist = TestFacilities.load_file_to_todolist("test/data/SorterTest1.txt")
def tearDown(self):
# restore to the default configuration in case a custom one was set
config("")
def test_sort1(self): def test_sort1(self):
""" Alphabetically sorted """ """ Alphabetically sorted """
command = SortCommand(["text"], self.todolist, self.out, self.error) command = SortCommand(["text"], self.todolist, self.out, self.error)
......
...@@ -36,10 +36,6 @@ class TodoListTester(TopydoTest): ...@@ -36,10 +36,6 @@ class TodoListTester(TopydoTest):
self.text = ''.join(lines) self.text = ''.join(lines)
self.todolist = TodoList(lines) self.todolist = TodoList(lines)
def tearDown(self):
# restore to the default configuration in case a custom one was set
config("")
def test_contexts(self): def test_contexts(self):
self.assertEquals(set(['Context1', 'Context2']), \ self.assertEquals(set(['Context1', 'Context2']), \
self.todolist.contexts()) self.todolist.contexts())
......
...@@ -19,9 +19,9 @@ import unittest ...@@ -19,9 +19,9 @@ import unittest
from topydo.lib.Config import config from topydo.lib.Config import config
class TopydoTest(unittest.TestCase): class TopydoTest(unittest.TestCase):
def setUp(self): def tearDown(self):
""" """
Make sure that every test case starts with a clean configuration. Make sure that every test case leaves a clean configuration.
""" """
config("") config("")
This diff was suppressed by a .gitattributes entry.
[topydo]
identifiers = text
[dep]
append_parent_contexts = 1
...@@ -9,6 +9,7 @@ highlight_projects_contexts = 1 ...@@ -9,6 +9,7 @@ highlight_projects_contexts = 1
identifiers = linenumber ; or: text identifiers = linenumber ; or: text
[ls] [ls]
hide_tags = id,p,ical
indent = 0 indent = 0
list_limit = -1 list_limit = -1
...@@ -27,3 +28,5 @@ ignore_weekends = 1 ...@@ -27,3 +28,5 @@ ignore_weekends = 1
[dep] [dep]
; Add parent projects when adding sub todo items ; Add parent projects when adding sub todo items
append_parent_projects = 0 append_parent_projects = 0
; Add parent contexts when adding sub todo items
append_parent_contexts = 0
...@@ -39,8 +39,10 @@ Available commands: ...@@ -39,8 +39,10 @@ Available commands:
* append (app) * append (app)
* del (rm) * del (rm)
* dep * dep
* depri
* do * do
* edit * edit
* ical
* ls * ls
* listcon (lscon) * listcon (lscon)
* listprojects (lsprj) * listprojects (lsprj)
...@@ -89,51 +91,14 @@ except ConfigError as config_error: ...@@ -89,51 +91,14 @@ except ConfigError as config_error:
error(str(config_error)) error(str(config_error))
sys.exit(1) sys.exit(1)
from topydo.lib.AddCommand import AddCommand from topydo.lib.Commands import get_subcommand
from topydo.lib.AppendCommand import AppendCommand
from topydo.lib.ArchiveCommand import ArchiveCommand from topydo.lib.ArchiveCommand import ArchiveCommand
from topydo.lib.DeleteCommand import DeleteCommand
from topydo.lib.DepCommand import DepCommand
from topydo.lib.DepriCommand import DepriCommand
from topydo.lib.DoCommand import DoCommand
from topydo.lib.EditCommand import EditCommand
from topydo.lib.ListCommand import ListCommand
from topydo.lib.ListContextCommand import ListContextCommand
from topydo.lib.ListProjectCommand import ListProjectCommand
from topydo.lib.PostponeCommand import PostponeCommand
from topydo.lib.PriorityCommand import PriorityCommand
from topydo.lib.SortCommand import SortCommand from topydo.lib.SortCommand import SortCommand
from topydo.lib.TagCommand import TagCommand
from topydo.lib import TodoFile from topydo.lib import TodoFile
from topydo.lib import TodoList from topydo.lib import TodoList
from topydo.lib import TodoListBase from topydo.lib import TodoListBase
from topydo.lib.Utils import escape_ansi from topydo.lib.Utils import escape_ansi
SUBCOMMAND_MAP = {
'add': AddCommand,
'app': AppendCommand,
'append': AppendCommand,
'del': DeleteCommand,
'dep': DepCommand,
'depri': DepriCommand,
'do': DoCommand,
'edit': EditCommand,
'ls': ListCommand,
'lscon': ListContextCommand,
'listcon': ListContextCommand,
'lsprj': ListProjectCommand,
'lsproj': ListProjectCommand,
'listprj': ListProjectCommand,
'listproj': ListProjectCommand,
'listproject': ListProjectCommand,
'listprojects': ListProjectCommand,
'postpone': PostponeCommand,
'pri': PriorityCommand,
'rm': DeleteCommand,
'sort': SortCommand,
'tag': TagCommand,
}
class CLIApplication(object): class CLIApplication(object):
""" """
Class that represents the Command Line Interface of Topydo. Class that represents the Command Line Interface of Topydo.
...@@ -212,52 +177,6 @@ class CLIApplication(object): ...@@ -212,52 +177,6 @@ class CLIApplication(object):
return False if command.execute() == False else True return False if command.execute() == False else True
def _get_subcommand(self, p_args):
"""
Retrieves the to-be executed Command and returns a tuple
(Command, args).
If args is an empty list, then the Command that corresponds with the
default command specified in the configuration will be returned.
If the first argument is 'help' and the second a valid subcommand, the
help text this function returns the Command instance of that subcommand
with a single argument 'help' (every Command has a help text).
If no valid command could be found, the subcommand part of the tuple
is None.
"""
result = None
args = p_args
try:
subcommand = p_args[0]
if subcommand in SUBCOMMAND_MAP:
result = SUBCOMMAND_MAP[subcommand]
args = args[1:]
elif subcommand == 'help':
try:
subcommand = args[1]
if subcommand in SUBCOMMAND_MAP:
args = [subcommand, 'help']
return self._get_subcommand(args)
except IndexError:
# will result in empty result
pass
else:
p_command = self.config.default_command()
if p_command in SUBCOMMAND_MAP:
result = SUBCOMMAND_MAP[p_command]
# leave args unchanged
except IndexError:
p_command = self.config.default_command()
if p_command in SUBCOMMAND_MAP:
result = SUBCOMMAND_MAP[p_command]
return (result, args)
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
args = self._process_flags() args = self._process_flags()
...@@ -265,7 +184,7 @@ class CLIApplication(object): ...@@ -265,7 +184,7 @@ class CLIApplication(object):
todofile = TodoFile.TodoFile(self.path) todofile = TodoFile.TodoFile(self.path)
self.todolist = TodoList.TodoList(todofile.read()) self.todolist = TodoList.TodoList(todofile.read())
(subcommand, args) = self._get_subcommand(args) (subcommand, args) = get_subcommand(args)
if subcommand == None: if subcommand == None:
usage() usage()
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -21,7 +21,7 @@ import re ...@@ -21,7 +21,7 @@ import re
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Command import Command from topydo.lib.Command import Command
from topydo.lib.PrettyPrinter import pretty_print from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
...@@ -93,7 +93,8 @@ class AddCommand(Command): ...@@ -93,7 +93,8 @@ class AddCommand(Command):
self.todo = self.todolist.add(self.text) self.todo = self.todolist.add(self.text)
self._postprocess_input_todo() self._postprocess_input_todo()
self.out(pretty_print(self.todo, [self.todolist.pp_number()])) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(self.printer.print_todo(self.todo))
else: else:
self.error(self.usage()) self.error(self.usage())
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinter import pretty_print from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
class AppendCommand(Command): class AppendCommand(Command):
...@@ -37,7 +37,9 @@ class AppendCommand(Command): ...@@ -37,7 +37,9 @@ class AppendCommand(Command):
if text: if text:
todo = self.todolist.todo(number) todo = self.todolist.todo(number)
self.todolist.append(todo, text) self.todolist.append(todo, text)
self.out(pretty_print(todo, [self.todolist.pp_number()]))
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(self.printer.print_todo(todo))
else: else:
self.error(self.usage()) self.error(self.usage())
except InvalidCommandArgument: except InvalidCommandArgument:
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
import getopt import getopt
from topydo.lib.PrettyPrinter import PrettyPrinter
class InvalidCommandArgument(Exception): class InvalidCommandArgument(Exception):
pass pass
...@@ -49,6 +51,9 @@ class Command(object): ...@@ -49,6 +51,9 @@ class Command(object):
self.error = p_err self.error = p_err
self.prompt = p_prompt self.prompt = p_prompt
# make pretty printer available
self.printer = PrettyPrinter()
def execute(self): def execute(self):
""" """
Execute the command. Intercepts the help subsubcommand to show the help Execute the command. Intercepts the help subsubcommand to show the help
......
# 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/>.
"""
This module is aware of all supported submodules and hands out a Command
instance based on an argument list.
"""
from topydo.lib.Config import config
from topydo.lib.AddCommand import AddCommand
from topydo.lib.AppendCommand import AppendCommand
from topydo.lib.DeleteCommand import DeleteCommand
from topydo.lib.DepCommand import DepCommand
from topydo.lib.DepriCommand import DepriCommand
from topydo.lib.DoCommand import DoCommand
from topydo.lib.EditCommand import EditCommand
from topydo.lib.IcalCommand import IcalCommand
from topydo.lib.ListCommand import ListCommand
from topydo.lib.ListContextCommand import ListContextCommand
from topydo.lib.ListProjectCommand import ListProjectCommand
from topydo.lib.PostponeCommand import PostponeCommand
from topydo.lib.PriorityCommand import PriorityCommand
from topydo.lib.SortCommand import SortCommand
from topydo.lib.TagCommand import TagCommand
_SUBCOMMAND_MAP = {
'add': AddCommand,
'app': AppendCommand,
'append': AppendCommand,
'del': DeleteCommand,
'dep': DepCommand,
'depri': DepriCommand,
'do': DoCommand,
'edit': EditCommand,
'ical': IcalCommand,
'ls': ListCommand,
'lscon': ListContextCommand,
'listcon': ListContextCommand,
'lsprj': ListProjectCommand,
'lsproj': ListProjectCommand,
'listprj': ListProjectCommand,
'listproj': ListProjectCommand,
'listproject': ListProjectCommand,
'listprojects': ListProjectCommand,
'postpone': PostponeCommand,
'pri': PriorityCommand,
'rm': DeleteCommand,
'sort': SortCommand,
'tag': TagCommand,
}
def get_subcommand(p_args):
"""
Retrieves the to-be executed Command and returns a tuple
(Command, args).
If args is an empty list, then the Command that corresponds with the
default command specified in the configuration will be returned.
If the first argument is 'help' and the second a valid subcommand, the
help text this function returns the Command instance of that subcommand
with a single argument 'help' (every Command has a help text).
If no valid command could be found, the subcommand part of the tuple
is None.
"""
result = None
args = p_args
try:
subcommand = p_args[0]
if subcommand in _SUBCOMMAND_MAP:
result = _SUBCOMMAND_MAP[subcommand]
args = args[1:]
elif subcommand == 'help':
try:
subcommand = args[1]
if subcommand in _SUBCOMMAND_MAP:
args = [subcommand, 'help']
return get_subcommand(args)
except IndexError:
# will result in empty result
pass
else:
p_command = config().default_command()
if p_command in _SUBCOMMAND_MAP:
result = _SUBCOMMAND_MAP[p_command]
# leave args unchanged
except IndexError:
p_command = config().default_command()
if p_command in _SUBCOMMAND_MAP:
result = _SUBCOMMAND_MAP[p_command]
return (result, args)
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -45,6 +45,7 @@ class _Config: ...@@ -45,6 +45,7 @@ class _Config:
'identifiers': 'linenumber', 'identifiers': 'linenumber',
# ls # ls
'hide_tags': 'id,p,ical',
'indent': 0, 'indent': 0,
'list_limit': '-1', 'list_limit': '-1',
...@@ -60,6 +61,7 @@ class _Config: ...@@ -60,6 +61,7 @@ class _Config:
# dep # dep
'append_parent_projects': '0', 'append_parent_projects': '0',
'append_parent_contexts': '0',
} }
self.config = {} self.config = {}
...@@ -147,6 +149,12 @@ class _Config: ...@@ -147,6 +149,12 @@ class _Config:
except ValueError: except ValueError:
return self.defaults['append_parent_projects'] == '1' return self.defaults['append_parent_projects'] == '1'
def append_parent_contexts(self):
try:
return self.cp.getboolean('dep', 'append_parent_contexts')
except ValueError:
return self.defaults['append_parent_contexts'] == '1'
def _get_tag(self, p_tag): def _get_tag(self, p_tag):
try: try:
return self.config[p_tag] return self.config[p_tag]
...@@ -164,6 +172,11 @@ class _Config: ...@@ -164,6 +172,11 @@ class _Config:
def tag_star(self): def tag_star(self):
return self._get_tag('tag_star') return self._get_tag('tag_star')
def hidden_tags(self):
""" Returns a list of tags to be hidden from the 'ls' output. """
hidden_tags = self.cp.get('ls', 'hide_tags')
return [] if hidden_tags == '' else hidden_tags.split(',')
def config(p_path=None): def config(p_path=None):
""" """
Retrieve the config instance. Retrieve the config instance.
......
...@@ -16,11 +16,12 @@ ...@@ -16,11 +16,12 @@
import re import re
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import pretty_print, pretty_print_list from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
class DCommand(Command): class DCommand(MultiCommand):
""" """
A common class for the 'do' and 'del' operations, because they're quite A common class for the 'do' and 'del' operations, because they're quite
alike. alike.
...@@ -37,13 +38,7 @@ class DCommand(Command): ...@@ -37,13 +38,7 @@ class DCommand(Command):
self.process_flags() self.process_flags()
self.length = len(self.todolist.todos()) # to determine newly activated todos self.length = len(self.todolist.todos()) # to determine newly activated todos
self.get_todos(self.args)
self.todos = []
for number in self.args:
try:
self.todos.append(self.todolist.todo(number))
except InvalidTodoException:
self.todos.append(None)
def get_flags(self): def get_flags(self):
""" Default implementation of getting specific flags. """ """ Default implementation of getting specific flags. """
...@@ -72,8 +67,9 @@ class DCommand(Command): ...@@ -72,8 +67,9 @@ class DCommand(Command):
) )
def _print_list(self, p_todos): def _print_list(self, p_todos):
filters = [self.todolist.pp_number()] printer = PrettyPrinter()
self.out("\n".join(pretty_print_list(p_todos, filters))) printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(printer.print_list(p_todos))
def prompt_text(self): def prompt_text(self):
return "Yes or no? [y/N] " return "Yes or no? [y/N] "
...@@ -93,7 +89,7 @@ class DCommand(Command): ...@@ -93,7 +89,7 @@ class DCommand(Command):
if not self.force and re.match('^y(es)?$', confirmation, re.I): if not self.force and re.match('^y(es)?$', confirmation, re.I):
for child in children: for child in children:
self.execute_specific_core(child) self.execute_specific_core(child)
self.out(self.prefix() + pretty_print(child)) self.out(self.prefix() + self.printer.print_todo(child))
def _print_unlocked_todos(self, p_old, p_new): def _print_unlocked_todos(self, p_old, p_new):
delta = [todo for todo in p_new if todo not in p_old] delta = [todo for todo in p_new if todo not in p_old]
...@@ -133,24 +129,15 @@ class DCommand(Command): ...@@ -133,24 +129,15 @@ class DCommand(Command):
""" """
pass pass
def execute(self): def execute_multi_specific(self):
if not super(DCommand, self).execute(): old_active = self._active_todos()
return False
if len(self.args) == 0:
self.error(self.usage())
else:
old_active = self._active_todos()
for todo in self.todos: for todo in self.todos:
if not todo: if todo and self.condition(todo):
self.error("Invalid todo number given.") self._process_subtasks(todo)
elif todo and self.condition(todo): self.execute_specific(todo)
self._process_subtasks(todo) else:
self.execute_specific(todo) self.error(self.condition_failed_text())
else:
self.error(self.condition_failed_text())
current_active = self._active_todos()
self._print_unlocked_todos(old_active, current_active)
current_active = self._active_todos()
self._print_unlocked_todos(old_active, current_active)
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.DCommand import DCommand from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import pretty_print
class DeleteCommand(DCommand): class DeleteCommand(DCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
...@@ -35,7 +34,7 @@ class DeleteCommand(DCommand): ...@@ -35,7 +34,7 @@ class DeleteCommand(DCommand):
self.todolist.delete(p_todo) self.todolist.delete(p_todo)
def execute_specific(self, p_todo): def execute_specific(self, p_todo):
self.out(self.prefix() + pretty_print(p_todo)) self.out(self.prefix() + self.printer.print_todo(p_todo))
self.execute_specific_core(p_todo) self.execute_specific_core(p_todo)
def usage(self): def usage(self):
......
...@@ -97,7 +97,8 @@ class DepCommand(Command): ...@@ -97,7 +97,8 @@ class DepCommand(Command):
if todos: if todos:
sorter = Sorter(config().sort_string()) sorter = Sorter(config().sort_string())
instance_filter = Filter.InstanceFilter(todos) instance_filter = Filter.InstanceFilter(todos)
view = View(sorter, [instance_filter], self.todolist) view = View(sorter, [instance_filter], self.todolist,
self.printer)
self.out(view.pretty_print()) self.out(view.pretty_print())
except InvalidTodoException: except InvalidTodoException:
self.error("Invalid todo number given.") self.error("Invalid todo number given.")
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -14,11 +14,10 @@ ...@@ -14,11 +14,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import pretty_print from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
class DepriCommand(Command): class DepriCommand(MultiCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
...@@ -26,28 +25,23 @@ class DepriCommand(Command): ...@@ -26,28 +25,23 @@ class DepriCommand(Command):
super(DepriCommand, self).__init__( super(DepriCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): self.get_todos(self.args)
if not super(DepriCommand, self).execute():
return False
todo = None def execute_multi_specific(self):
try: try:
todo = self.todolist.todo(self.argument(0)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if todo.priority() != None: for todo in self.todos:
self.todolist.set_priority(todo, None) if todo.priority() != None:
self.out("Priority removed.") self.todolist.set_priority(todo, None)
self.out(pretty_print(todo)) self.out("Priority removed.")
except InvalidCommandArgument: self.out(self.printer.print_todo(todo))
except IndexError:
self.error(self.usage()) self.error(self.usage())
except (InvalidTodoException):
if not todo:
self.error( "Invalid todo number given.")
else:
self.error(self.usage())
def usage(self): def usage(self):
return """Synopsis: depri <NUMBER>""" return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]"""
def help(self): def help(self):
return """Removes the priority of the given todo item.""" return """Removes the priority of the given todo item(s)."""
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
from datetime import date from datetime import date
from topydo.lib.DCommand import DCommand from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import pretty_print 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, strict_advance_recurring_todo, NoRecurrenceException
from topydo.lib.Utils import date_string_to_date from topydo.lib.Utils import date_string_to_date
...@@ -57,7 +58,10 @@ class DoCommand(DCommand): ...@@ -57,7 +58,10 @@ class DoCommand(DCommand):
self.completion_date) self.completion_date)
self.todolist.add_todo(new_todo) self.todolist.add_todo(new_todo)
self.out(pretty_print(new_todo, [self.todolist.pp_number()]))
printer = PrettyPrinter()
printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(printer.print_todo(new_todo))
except NoRecurrenceException: except NoRecurrenceException:
self.error("Warning: todo item has an invalid recurrence pattern.") self.error("Warning: todo item has an invalid recurrence pattern.")
...@@ -80,7 +84,9 @@ class DoCommand(DCommand): ...@@ -80,7 +84,9 @@ class DoCommand(DCommand):
""" Actions specific to this command. """ """ Actions specific to this command. """
self._handle_recurrence(p_todo) self._handle_recurrence(p_todo)
self.execute_specific_core(p_todo) self.execute_specific_core(p_todo)
self.out(self.prefix() + pretty_print(p_todo))
printer = PrettyPrinter()
self.out(self.prefix() + printer.print_todo(p_todo))
def execute_specific_core(self, p_todo): def execute_specific_core(self, p_todo):
""" """
......
...@@ -15,31 +15,152 @@ ...@@ -15,31 +15,152 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
from subprocess import call from subprocess import call, check_call, CalledProcessError
import tempfile
from topydo.lib.Command import Command from topydo.lib.ListCommand import ListCommand
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Todo import Todo
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.TodoList import TodoList
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
class EditCommand(Command): class EditCommand(MultiCommand, ListCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(EditCommand, self).__init__(p_args, p_todolist, p_output, super(EditCommand, self).__init__(p_args, p_todolist, p_output,
p_error, p_input) p_error, p_input)
self.is_expression = False
self.edit_archive = 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
self.args = args
def _todos_to_temp(self):
f = tempfile.NamedTemporaryFile()
for todo in self.todos:
f.write("%s\n" % todo.__str__())
f.seek(0)
return f
def _todos_from_temp(self, temp_file):
temp_file.seek(0)
todos = temp_file.read().splitlines()
todo_objs = []
for todo in todos:
todo_objs.append(Todo(todo))
return todo_objs
def _open_in_editor(self, temp_file, editor):
try:
return check_call([editor, temp_file.name])
except(CalledProcessError):
self.error('Something went wrong in the editor...')
return 1
def _catch_todo_errors(self):
errors = []
if len(self.invalid_numbers) > 1 or len(self.invalid_numbers) > 0 and len(self.todos) > 0:
for number in self.invalid_numbers:
errors.append("Invalid todo number given: {}.".format(number))
elif len(self.invalid_numbers) == 1 and len(self.todos) == 0:
errors.append("Invalid todo number given.")
if len(errors) > 0:
return errors
else:
return None
def execute(self): def execute(self):
if not super(EditCommand, self).execute(): if not super(ListCommand, self).execute():
return False return False
editor = os.environ['EDITOR'] or 'vi' self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
todo = config().todotxt() try:
editor = os.environ['EDITOR'] or 'vi'
except(KeyError):
editor = 'vi'
try:
if len(self.args) < 1:
todo = config().todotxt()
return call([editor, todo]) == 0
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)
return call([editor, todo]) == 0 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:
super(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')
def usage(self): def usage(self):
return """Synopsis: edit""" return """Synopsis:
edit
edit <NUMBER1> [<NUMBER2> ...]
edit -e [-x] [expression]
edit -d"""
def help(self): def help(self):
return """Launches a text editor with the todo.txt file. return """\
Launches a text editor to edit todos.
Without any arguments it will just open the todo.txt file. Alternatively it can
edit todo item(s) with the given number(s) or edit relevant todos matching
the given expression. See `topydo help ls` for more information on relevant
todo items. It is also possible to open the archive file.
By default it will use $EDITOR in your environment, otherwise it will fall back By default it will use $EDITOR in your environment, otherwise it will fall back
to 'vi'. to 'vi'.
-e : Treat the subsequent arguments as an expression.
-x : Edit *all* todos matching the expression (i.e. do not filter on
dependencies or relevance).
-d : Open the archive file.
""" """
...@@ -196,3 +196,4 @@ class OrdinalTagFilter(Filter): ...@@ -196,3 +196,4 @@ class OrdinalTagFilter(Filter):
return operand1 != operand2 return operand1 != operand2
return False return False
...@@ -20,7 +20,7 @@ value of each item. ...@@ -20,7 +20,7 @@ value of each item.
""" """
_TABLE_SIZES = { _TABLE_SIZES = {
# we choose a large table size to reduce the change of collisions. # we choose a large table size to reduce the chance of collisions.
3: 46649, # largest prime under zzz_36 3: 46649, # largest prime under zzz_36
4: 1679609 # largest prime under zzzz_36 4: 1679609 # largest prime under zzzz_36
} }
......
# 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/>.
"""
Implements a subcommand that outputs an iCalendar file.
"""
from topydo.lib.IcalPrinter import IcalPrinter
from topydo.lib.ListCommand import ListCommand
class IcalCommand(ListCommand):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(IcalCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = IcalPrinter(p_todolist)
def _print(self):
self.out(str(self._view()))
def execute(self):
try:
import icalendar as _
except ImportError:
self.error("icalendar package is not installed.")
return False
return super(IcalCommand, self).execute()
def usage(self):
return """Synopsis: ical [-x] [expression]"""
def help(self):
return """\
Similar to the 'ls' subcommand, except that the todos are printed in iCalendar
format (RFC 2445) that can be imported by other calendar applications.
By default prints the active todo items, possibly filtered by the given
expression.
For the supported options, please refer to the help text of 'ls'
(topydo help ls).
While specifying the sort order is supported (-s flag), like in 'ls', this is
not meaningful in the context of an iCalendar file.
Note: be aware that this is not necessarily a read-only operation. This
subcommand may add ical tags to the printed todo items containing a unique ID.
Completed todo items may be archived.
Note: topydo does not support reading iCal files, this is merely a dump.
Changes made with other iCalendar enabled applications will not be processed.
Suggested usage is to use the output as a read-only calendar.
"""
# 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/>.
"""
Provides a printer that transforms a list of Todo items to an iCalendar
file according to RFC 2445.
"""
try:
import icalendar as ical
ICAL_PRESENT = True
except ImportError:
ICAL_PRESENT = False
from datetime import datetime, time
import random
import string
from topydo.lib.PrettyPrinter import Printer
def _convert_priority(p_priority):
"""
Converts todo.txt priority to an iCalendar priority (RFC 2445).
Priority A gets priority 1, priority B gets priority 5 and priority C-F get
priorities 6-9. This scheme makes sure that clients that use "high",
"medium" and "low" show the correct priority.
"""
result = 0
prio_map = {
'A': 1,
'B': 5,
'C': 6,
'D': 7,
'E': 8,
'F': 9,
}
try:
result = prio_map[p_priority]
except KeyError:
if p_priority:
# todos with no priority have priority None, and result of this
# function will be 0. For all other letters, return 9 (lowest
# priority in RFC 2445).
result = 9
return result
class IcalPrinter(Printer):
"""
A printer that converts a list of Todo items to a string in iCalendar
format (RFC 2445).
https://www.rfc-editor.org/rfc/rfc2445.txt
"""
def __init__(self, p_todolist):
super(IcalPrinter, self).__init__()
self.todolist = p_todolist
def print_todo(self, p_todo):
return self._convert_todo(p_todo).to_ical() if ICAL_PRESENT else ""
def print_list(self, p_todos):
result = ""
if ICAL_PRESENT:
cal = ical.Calendar()
cal.add('prodid', '-//bramschoenmakers.nl//topydo//')
cal.add('version', '2.0')
for todo in p_todos:
cal.add_component(self._convert_todo(todo))
result = cal.to_ical()
return result
def _convert_todo(self, p_todo):
""" Converts a Todo instance (Topydo) to an icalendar Todo instance. """
def _get_uid(p_todo):
"""
Gets a unique ID from a todo item, stored by the ical tag. If the
tag is not present, a random value is assigned to it and returned.
"""
def generate_uid(p_length=4):
"""
Generates a random string of the given length, used as
identifier.
"""
return ''.join(
random.choice(string.ascii_letters + string.digits)
for i in xrange(p_length))
uid = p_todo.tag_value('ical')
if not uid:
uid = generate_uid()
p_todo.set_tag('ical', uid)
self.todolist.set_dirty()
return uid
result = ical.Todo()
# this should be called first, it may set the ical: tag and therefore
# change the source() output.
result['uid'] = _get_uid(p_todo)
result['summary'] = ical.vText(p_todo.text())
result['description'] = ical.vText(p_todo.source())
result.add('priority', _convert_priority(p_todo.priority()))
start = p_todo.start_date()
if start:
result.add('dtstart', start)
due = p_todo.due_date()
if due:
result.add('due', due)
created = p_todo.creation_date()
if created:
result.add('created', created)
completed = p_todo.completion_date()
if completed:
completed = datetime.combine(completed, time(0, 0))
result.add('completed', completed)
return result
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -19,8 +19,12 @@ import re ...@@ -19,8 +19,12 @@ import re
from topydo.lib.Command import Command from topydo.lib.Command import Command
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib import Filter from topydo.lib import Filter
from topydo.lib.PrettyPrinter import pp_indent from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterIndentFilter,
PrettyPrinterHideTagFilter
)
from topydo.lib.Sorter import Sorter from topydo.lib.Sorter import Sorter
from topydo.lib.View import View
class ListCommand(Command): class ListCommand(Command):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
...@@ -74,17 +78,29 @@ class ListCommand(Command): ...@@ -74,17 +78,29 @@ class ListCommand(Command):
return filters return filters
def _view(self):
sorter = Sorter(self.sort_expression)
filters = self._filters()
return View(sorter, filters, self.todolist, self.printer)
def _print(self):
""" Prints the todos. """
indent = config().list_indent()
hidden_tags = config().hidden_tags()
filters = []
filters.append(PrettyPrinterIndentFilter(indent))
filters.append(PrettyPrinterHideTagFilter(hidden_tags))
self.out(self._view().pretty_print(filters))
def execute(self): def execute(self):
if not super(ListCommand, self).execute(): if not super(ListCommand, self).execute():
return False return False
self._process_flags() self._process_flags()
self._print()
sorter = Sorter(self.sort_expression)
filters = self._filters()
pp_filters = [pp_indent(config().list_indent())]
self.out(self.todolist.view(sorter, filters).pretty_print(pp_filters))
def usage(self): def usage(self):
return """Synopsis: ls [-x] [-s <sort_expression>] [expression]""" return """Synopsis: ls [-x] [-s <sort_expression>] [expression]"""
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 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.Command import Command
from topydo.lib.TodoListBase import InvalidTodoException
class MultiCommand(Command):
"""
A common class for operations that can work with multiple todo IDs.
"""
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(MultiCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.todos = []
self.invalid_numbers = []
def get_todos(self, p_numbers):
""" 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)
def _catch_todo_errors(self):
"""
Returns None or list of error messages depending on number of valid todo
objects and number of invalid todo IDs.
In case of multiple invalid todo IDs we generate separate error message for each
one of them with information about supplied ID.
"""
errors = []
if len(self.invalid_numbers) > 1 or len(self.invalid_numbers) > 0 and len(self.todos) > 0:
for number in self.invalid_numbers:
errors.append("Invalid todo number given: {}.".format(number))
elif len(self.invalid_numbers) == 1 and len(self.todos) == 0:
errors.append("Invalid todo number given.")
elif len(self.todos) == 0 and len(self.invalid_numbers) == 0:
errors.append(self.usage())
if len(errors) > 0:
return errors
else:
return None
def execute_multi_specific(self):
"""
Operations specific for particular command dealing with multiple todo
IDs.
"""
pass
def execute(self):
if not super(MultiCommand, self).execute():
return False
todo_errors = self._catch_todo_errors()
if not todo_errors:
self.execute_multi_specific()
else:
for error in todo_errors:
self.error(error)
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -16,14 +16,15 @@ ...@@ -16,14 +16,15 @@
from datetime import date, timedelta from datetime import date, timedelta
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.Command import InvalidCommandArgument
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.PrettyPrinter import pretty_print from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import date_string_to_date from topydo.lib.Utils import date_string_to_date
class PostponeCommand(Command): class PostponeCommand(MultiCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
...@@ -32,6 +33,8 @@ class PostponeCommand(Command): ...@@ -32,6 +33,8 @@ class PostponeCommand(Command):
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False self.move_start_date = False
self._process_flags()
self.get_todos(self.args[:-1])
def _process_flags(self): def _process_flags(self):
opts, args = self.getopt('s') opts, args = self.getopt('s')
...@@ -42,7 +45,7 @@ class PostponeCommand(Command): ...@@ -42,7 +45,7 @@ class PostponeCommand(Command):
self.args = args self.args = args
def execute(self): def execute_multi_specific(self):
def _get_offset(p_todo): def _get_offset(p_todo):
offset = p_todo.tag_value( offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat()) config().tag_due(), date.today().isoformat())
...@@ -53,45 +56,38 @@ class PostponeCommand(Command): ...@@ -53,45 +56,38 @@ class PostponeCommand(Command):
return offset_date return offset_date
if not super(PostponeCommand, self).execute():
return False
self._process_flags()
try: try:
todo = self.todolist.todo(self.argument(0)) pattern = self.args[-1]
pattern = self.argument(1) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
# pdb.set_trace() for todo in self.todos:
offset = _get_offset(todo) offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset) new_due = relative_date_to_date(pattern, offset)
if new_due: if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()): if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length() length = todo.length()
new_start = new_due - timedelta(length) new_start = new_due - timedelta(length)
todo.set_tag(config().tag_start(), new_start.isoformat()) todo.set_tag(config().tag_start(), new_start.isoformat())
todo.set_tag(config().tag_due(), new_due.isoformat()) todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty() self.todolist.set_dirty()
self.out(pretty_print(todo, [self.todolist.pp_number()])) self.out(self.printer.print_todo(todo))
else: else:
self.error("Invalid date pattern given.") self.error("Invalid date pattern given.")
break
except InvalidCommandArgument: except (InvalidCommandArgument, IndexError):
self.error(self.usage()) self.error(self.usage())
except (InvalidTodoException):
self.error("Invalid todo number given.")
def usage(self): def usage(self):
return "Synopsis: postpone [-s] <NUMBER> <PATTERN>" return "Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
def help(self): def help(self):
return """\ return """\
Postpone a todo item with the given number and the given pattern. Postpone the todo item(s) with the given number(s) and the given pattern.
Postponing is done by adjusting the due date of the todo, and if the -s flag is Postponing is done by adjusting the due date(s) of the todo(s), and if the -s flag is
given, the start date accordingly. given, the start date accordingly.
The pattern is a relative date, written in the format <COUNT><PERIOD> where The pattern is a relative date, written in the format <COUNT><PERIOD> where
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -14,75 +14,52 @@ ...@@ -14,75 +14,52 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Provides a function to pretty print a list of todo items. """ class Printer(object):
import re
from topydo.lib.Config import config
PRIORITY_COLORS = {
'A': '\033[36m', # cyan
'B': '\033[33m', # yellow
'C': '\033[34m' # blue
}
PROJECT_COLOR = '\033[31m' # red
NEUTRAL_COLOR = '\033[0m'
def pp_color(p_todo_str, p_todo):
""" """
Adds colors to the todo string by inserting ANSI codes. An abstract class that turns todo items into strings.
Should be passed as a filter in the filter list of pretty_print() Subclasses must at least implement the print_todo method.
""" """
def print_todo(self, p_todo):
if config().colors(): """ Base implementation. Simply returns the string conversion. """
color = NEUTRAL_COLOR return str(p_todo)
try:
color = PRIORITY_COLORS[p_todo.priority()] def print_list(self, p_todos):
except KeyError: """
pass Given a list of todo items, pretty print it and return a list of
formatted strings.
p_todo_str = color + p_todo_str + NEUTRAL_COLOR """
return "\n".join([self.print_todo(todo) for todo in p_todos])
if config().highlight_projects_contexts():
p_todo_str = re.sub( class PrettyPrinter(Printer):
r'\B(\+|@)(\S*\w)',
PROJECT_COLOR + r'\g<0>' + color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR
return p_todo_str
def pp_indent(p_indent=0):
return lambda s, t: ' ' * p_indent + s
def pretty_print(p_todo, p_filters=None):
""" """
Given a todo item, pretty print it and return a list of formatted strings. Prints todo items on a single line, decorated by the filters passed by
the caller.
p_filters is a list of functions that transform the output string, each
function accepting two arguments:
* the todo's text that has to be modified; The caller can adjust the output by passing on a set of filters, that may
* the todo object itself which allows for obtaining relevant information. add colors, indentation, etc. These filters are found in the
PrettyPrinterFilter module.
Example is pp_color in this fle.
""" """
p_filters = p_filters or [] def __init__(self):
"""
Constructor.
"""
super(PrettyPrinter, self).__init__()
self.filters = []
todo_str = str(p_todo) def add_filter(self, p_filter):
"""
Adds a filter to be applied when calling print_todo.
for f in p_filters: p_filter is an instance of a PrettyPrinterFilter.
todo_str = f(todo_str, p_todo) """
self.filters.append(p_filter)
return todo_str def print_todo(self, p_todo):
""" Given a todo item, pretty print it. """
todo_str = str(p_todo)
def pretty_print_list(p_todos, p_filters=None): for ppf in self.filters:
""" todo_str = ppf.filter(todo_str, p_todo)
Given a list of todo items, pretty print it and return a list of
formatted strings. return todo_str
"""
p_filters = p_filters or []
return [pretty_print(todo, p_filters) for todo in p_todos]
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 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/>.
""" Provides filters used for pretty printing. """
import re
from topydo.lib.Config import config
class PrettyPrinterFilter(object):
"""
Base class for a pretty printer filter.
Subclasses must reimplement the filter method.
"""
def filter(self, p_todo_str, _):
""" Default implementation returns an unmodified todo string. """
return p_todo_str
PRIORITY_COLORS = {
'A': '\033[36m', # cyan
'B': '\033[33m', # yellow
'C': '\033[34m' # blue
}
PROJECT_COLOR = '\033[31m' # red
NEUTRAL_COLOR = '\033[0m'
class PrettyPrinterColorFilter(PrettyPrinterFilter):
"""
Adds colors to the todo string by inserting ANSI codes.
Should be passed as a filter in the filter list of pretty_print()
"""
def filter(self, p_todo_str, p_todo):
""" Applies the colors. """
if config().colors():
color = NEUTRAL_COLOR
try:
color = PRIORITY_COLORS[p_todo.priority()]
except KeyError:
pass
p_todo_str = color + p_todo_str + NEUTRAL_COLOR
if config().highlight_projects_contexts():
p_todo_str = re.sub(
r'\B(\+|@)(\S*\w)',
PROJECT_COLOR + r'\g<0>' + color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR
return p_todo_str
class PrettyPrinterIndentFilter(PrettyPrinterFilter):
""" Adds indentation to the todo item. """
def __init__(self, p_indent=0):
super(PrettyPrinterIndentFilter, self).__init__()
self.indent = p_indent
def filter(self, p_todo_str, _):
""" Applies the indentation. """
return ' ' * self.indent + p_todo_str
class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__()
self.todolist = p_todolist
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)
class PrettyPrinterHideTagFilter(PrettyPrinterFilter):
""" Removes all occurences of the given tags from the text. """
def __init__(self, p_hidden_tags):
super(PrettyPrinterHideTagFilter, self).__init__()
self.hidden_tags = p_hidden_tags
def filter(self, p_todo_str, _):
for hidden_tag in self.hidden_tags:
# inspired from remove_tag in TodoBase
p_todo_str = re.sub(r'\s?\b' + hidden_tag + r':\S+\b', '',
p_todo_str)
return p_todo_str
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -14,12 +14,12 @@ ...@@ -14,12 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import pretty_print from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import is_valid_priority from topydo.lib.Utils import is_valid_priority
class PriorityCommand(Command): class PriorityCommand(MultiCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
...@@ -27,42 +27,36 @@ class PriorityCommand(Command): ...@@ -27,42 +27,36 @@ class PriorityCommand(Command):
super(PriorityCommand, self).__init__( super(PriorityCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): self.get_todos(self.args[:-1])
if not super(PriorityCommand, self).execute():
return False
number = None def execute_multi_specific(self):
priority = None priority = None
try: try:
number = self.argument(0) priority = self.args[-1]
priority = self.argument(1) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
todo = self.todolist.todo(number)
if is_valid_priority(priority): if is_valid_priority(priority):
old_priority = todo.priority() for todo in self.todos:
self.todolist.set_priority(todo, priority) old_priority = todo.priority()
self.todolist.set_priority(todo, priority)
if old_priority and priority and old_priority != priority: if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format( self.out("Priority changed from {} to {}".format(
old_priority, priority)) old_priority, priority))
elif not old_priority: elif not old_priority:
self.out("Priority set to {}.".format(priority)) self.out("Priority set to {}.".format(priority))
self.out(pretty_print(todo)) self.out(self.printer.print_todo(todo))
else: else:
self.error("Invalid priority given.") self.error("Invalid priority given.")
except InvalidCommandArgument: except IndexError:
self.error(self.usage()) self.error(self.usage())
except (InvalidTodoException):
if number and priority:
self.error( "Invalid todo number given.")
else:
self.error(self.usage())
def usage(self): def usage(self):
return """Synopsis: pri <NUMBER> <PRIORITY>""" return """Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>"""
def help(self): def help(self):
return """\ return """\
Sets the priority of todo the given number to the given priority. Sets the priority of todo(s) the given number(s) to the given priority.
""" """
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.PrettyPrinter import pretty_print
class TagCommand(Command): class TagCommand(Command):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
...@@ -62,7 +62,8 @@ class TagCommand(Command): ...@@ -62,7 +62,8 @@ class TagCommand(Command):
self.value = "" self.value = ""
def _print(self): def _print(self):
self.out(pretty_print(self.todo, [self.todolist.pp_number()])) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(self.printer.print_todo(self.todo))
def _choose(self): def _choose(self):
""" """
......
...@@ -141,6 +141,15 @@ class TodoList(TodoListBase): ...@@ -141,6 +141,15 @@ class TodoList(TodoListBase):
for project in p_from_todo.projects() - p_to_todo.projects(): for project in p_from_todo.projects() - p_to_todo.projects():
self.append(p_to_todo, "+{}".format(project)) self.append(p_to_todo, "+{}".format(project))
def append_contexts_to_subtodo():
"""
Appends contexts in the parent todo item that are not present in
the sub todo item.
"""
if config().append_parent_contexts():
for context in p_from_todo.contexts() - p_to_todo.contexts():
self.append(p_to_todo, "@{}".format(context))
if p_from_todo != p_to_todo and not self._depgraph.has_edge( if p_from_todo != p_to_todo and not self._depgraph.has_edge(
hash(p_from_todo), hash(p_to_todo)): hash(p_from_todo), hash(p_to_todo)):
...@@ -155,6 +164,7 @@ class TodoList(TodoListBase): ...@@ -155,6 +164,7 @@ class TodoList(TodoListBase):
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id) self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id)
self._update_parent_cache() self._update_parent_cache()
append_projects_to_subtodo() append_projects_to_subtodo()
append_contexts_to_subtodo()
self.dirty = True self.dirty = True
def remove_dependency(self, p_from_todo, p_to_todo): def remove_dependency(self, p_from_todo, p_to_todo):
......
...@@ -24,7 +24,7 @@ import re ...@@ -24,7 +24,7 @@ import re
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib import Filter from topydo.lib import Filter
from topydo.lib.HashListValues import hash_list_values from topydo.lib.HashListValues import hash_list_values
from topydo.lib.PrettyPrinter import pretty_print_list from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.Todo import Todo from topydo.lib.Todo import Todo
from topydo.lib.View import View from topydo.lib.View import View
...@@ -242,14 +242,6 @@ class TodoListBase(object): ...@@ -242,14 +242,6 @@ class TodoListBase(object):
except (ValueError, KeyError): except (ValueError, KeyError):
raise InvalidTodoException raise InvalidTodoException
def pp_number(self):
"""
A filter for the pretty printer to append the todo number to the
printed todo.
"""
return lambda p_todo_str, p_todo: \
"|{:>3}| {}".format(self.number(p_todo), p_todo_str)
def _update_todo_ids(self): def _update_todo_ids(self):
# the idea is to have a hash that is independent of the position of the # the idea is to have a hash that is independent of the position of the
# todo. Use the text (without tags) of the todo to keep the id as stable # todo. Use the text (without tags) of the todo to keep the id as stable
...@@ -264,5 +256,6 @@ class TodoListBase(object): ...@@ -264,5 +256,6 @@ class TodoListBase(object):
self._id_todo_map[uid] = todo self._id_todo_map[uid] = todo
def __str__(self): def __str__(self):
return '\n'.join(pretty_print_list(self._todos)) printer = PrettyPrinter()
return printer.print_list(self._todos)
""" Version of Topydo. """ """ Version of Topydo. """
VERSION = '0.2' VERSION = '0.3'
LICENSE = """Copyright (C) 2014 Bram Schoenmakers LICENSE = """Copyright (C) 2014 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -16,19 +16,29 @@ ...@@ -16,19 +16,29 @@
""" A view is a list of todos, sorted and filtered. """ """ A view is a list of todos, sorted and filtered. """
from topydo.lib.PrettyPrinter import pretty_print_list, pp_color from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterColorFilter,
PrettyPrinterNumbers
)
from topydo.lib.PrettyPrinter import PrettyPrinter
class View(object): class View(object):
""" """
A view is instantiated by a todo list, usually obtained from a todo.txt 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 file. Also a sorter and a list of filters should be given that is applied
to the list. 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): def __init__(self, p_sorter, p_filters, p_todolist,
p_printer=PrettyPrinter()):
self._todolist = p_todolist self._todolist = p_todolist
self._viewdata = [] self._viewdata = []
self._sorter = p_sorter self._sorter = p_sorter
self._filters = p_filters self._filters = p_filters
self._printer = p_printer
self.update() self.update()
...@@ -45,8 +55,20 @@ class View(object): ...@@ -45,8 +55,20 @@ class View(object):
def pretty_print(self, p_pp_filters=None): def pretty_print(self, p_pp_filters=None):
""" Pretty prints the view. """ """ Pretty prints the view. """
p_pp_filters = p_pp_filters or [] p_pp_filters = p_pp_filters or []
pp_filters = [self._todolist.pp_number(), pp_color] + p_pp_filters
return '\n'.join(pretty_print_list(self._viewdata, pp_filters)) # 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)
def __str__(self): def __str__(self):
return '\n'.join(pretty_print_list(self._viewdata)) return self._printer.print_list(self._viewdata)
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