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
*.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
---
* 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.
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
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
......@@ -15,7 +35,7 @@
Sowiński).
* Multiple items can be marked as complete or deleted at once.
* 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 <subcommand>` as well.
* Let setuptools provide a `topydo` executable.
......
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
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
on top of the CLI, hereafter refered to as todo.txt-tools. These extensions
are:
on top of the CLI. These extensions are:
* Set **due** and **start dates**;
* Custom sorting;
* Dealing with tags;
* 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
**relative dates**);
......
......@@ -3,11 +3,15 @@ from setuptools import setup
setup(
name = "topydo",
packages = ["topydo", "topydo.lib", "topydo.cli"],
version = "0.2",
version = "0.3",
description = "A command-line todo list application using the todo.txt format.",
author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo",
extras_require = {
'ical': ['icalendar'],
'edit-cmd-tests': ['mock'],
},
entry_points= {
'console_scripts': ['topydo = topydo.cli.Main:main'],
},
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -190,7 +190,27 @@ class AddCommandTest(CommandTest.CommandTest):
command = ListCommand.ListCommand(["Bar"], self.todolist, self.out, self.error)
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):
command = AddCommand.AddCommand(["Foo due:today"], self.todolist, self.out, self.error)
......
......@@ -133,11 +133,22 @@ class DeleteCommandTest(CommandTest.CommandTest):
self.assertEquals(self.todolist.count(), 0)
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.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):
command = DeleteCommand([], self.todolist, self.out, self.error)
......
......@@ -26,20 +26,21 @@ class DepriCommandTest(CommandTest.CommandTest):
todos = [
"(A) Foo",
"Bar",
"(B) Baz",
]
self.todolist = TodoList(todos)
def test_set_prio1(self):
def test_depri1(self):
command = DepriCommand(["1"], 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.output, "Priority removed.\nFoo\n")
self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEquals(self.errors, "")
def test_set_prio2(self):
def test_depri2(self):
command = DepriCommand(["2"], self.todolist, self.out, self.error)
command.execute()
......@@ -48,15 +49,26 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "")
self.assertEquals(self.errors, "")
def test_set_prio3(self):
def test_depri3(self):
command = DepriCommand(["Foo"], 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.output, "Priority removed.\nFoo\n")
self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
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):
command = DepriCommand(["99"], self.todolist, self.out, self.error)
command.execute()
......@@ -65,6 +77,22 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output)
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):
command = DepriCommand([], self.todolist, self.out, self.error)
command.execute()
......
......@@ -312,20 +312,17 @@ class DoCommandTest(CommandTest.CommandTest):
command = DoCommand(["99", "3"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertTrue(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.\n")
self.assertFalse(self.todolist.todo(3).is_completed())
self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
def test_multi_do5(self):
"""
When a todo item was generated by a recurring todo item, make sure
it cannot be completed in the same invocation.
Check output when all supplied todo numbers are invalid.
"""
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()
self.assertTrue(self.todolist.todo(4).is_completed())
self.assertFalse(self.todolist.todo(10).is_completed())
self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 10.\n")
def test_invalid_recurrence(self):
""" 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):
super(ListCommandTest, self).setUp()
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):
command = ListCommand([""], self.todolist, self.out, self.error)
command.execute()
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, "")
def test_list3(self):
......@@ -43,7 +39,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list4(self):
......@@ -51,7 +47,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list5(self):
......@@ -59,7 +55,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list6(self):
......@@ -75,7 +71,7 @@ class ListCommandTest(CommandTest.CommandTest):
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.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEquals(self.errors, "")
def test_list8(self):
......@@ -83,7 +79,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list9(self):
......@@ -91,7 +87,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list10(self):
......@@ -99,7 +95,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list11(self):
......@@ -119,7 +115,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list13(self):
......@@ -127,7 +123,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list14(self):
......@@ -137,7 +133,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list15(self):
......@@ -145,7 +141,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list16(self):
......@@ -155,7 +151,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list17(self):
......@@ -163,7 +159,7 @@ class ListCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_list18(self):
......@@ -173,6 +169,17 @@ class ListCommandTest(CommandTest.CommandTest):
self.assertFalse(self.todolist.is_dirty())
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):
command = ListCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -164,6 +164,61 @@ class PostponeCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "| 1| Foo due:{}\n".format(due.isoformat()))
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):
command = PostponeCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -35,7 +35,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_set_prio2(self):
......@@ -43,7 +43,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_set_prio3(self):
......@@ -51,7 +51,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_set_prio4(self):
......@@ -59,7 +59,15 @@ class PriorityCommandTest(CommandTest.CommandTest):
command.execute()
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, "")
def test_invalid1(self):
......@@ -71,6 +79,22 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertEquals(self.errors, "Invalid todo number given.\n")
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.execute()
......@@ -78,7 +102,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output)
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.execute()
......@@ -86,7 +110,7 @@ class PriorityCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output)
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.execute()
......
......@@ -26,10 +26,6 @@ class SortCommandTest(CommandTest.CommandTest):
super(SortCommandTest, self).setUp()
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):
""" Alphabetically sorted """
command = SortCommand(["text"], self.todolist, self.out, self.error)
......
......@@ -36,10 +36,6 @@ class TodoListTester(TopydoTest):
self.text = ''.join(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):
self.assertEquals(set(['Context1', 'Context2']), \
self.todolist.contexts())
......
......@@ -19,9 +19,9 @@ import unittest
from topydo.lib.Config import config
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("")
This diff was suppressed by a .gitattributes entry.
[topydo]
identifiers = text
[dep]
append_parent_contexts = 1
......@@ -9,6 +9,7 @@ highlight_projects_contexts = 1
identifiers = linenumber ; or: text
[ls]
hide_tags = id,p,ical
indent = 0
list_limit = -1
......@@ -27,3 +28,5 @@ ignore_weekends = 1
[dep]
; Add parent projects when adding sub todo items
append_parent_projects = 0
; Add parent contexts when adding sub todo items
append_parent_contexts = 0
......@@ -39,8 +39,10 @@ Available commands:
* append (app)
* del (rm)
* dep
* depri
* do
* edit
* ical
* ls
* listcon (lscon)
* listprojects (lsprj)
......@@ -89,51 +91,14 @@ except ConfigError as config_error:
error(str(config_error))
sys.exit(1)
from topydo.lib.AddCommand import AddCommand
from topydo.lib.AppendCommand import AppendCommand
from topydo.lib.Commands import get_subcommand
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.TagCommand import TagCommand
from topydo.lib import TodoFile
from topydo.lib import TodoList
from topydo.lib import TodoListBase
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 that represents the Command Line Interface of Topydo.
......@@ -212,52 +177,6 @@ class CLIApplication(object):
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):
""" Main entry function. """
args = self._process_flags()
......@@ -265,7 +184,7 @@ class CLIApplication(object):
todofile = TodoFile.TodoFile(self.path)
self.todolist = TodoList.TodoList(todofile.read())
(subcommand, args) = self._get_subcommand(args)
(subcommand, args) = get_subcommand(args)
if subcommand == None:
usage()
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -21,7 +21,7 @@ import re
from topydo.lib.Config import config
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.TodoListBase import InvalidTodoException
......@@ -93,7 +93,8 @@ class AddCommand(Command):
self.todo = self.todolist.add(self.text)
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:
self.error(self.usage())
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
class AppendCommand(Command):
......@@ -37,7 +37,9 @@ class AppendCommand(Command):
if text:
todo = self.todolist.todo(number)
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:
self.error(self.usage())
except InvalidCommandArgument:
......
......@@ -16,6 +16,8 @@
import getopt
from topydo.lib.PrettyPrinter import PrettyPrinter
class InvalidCommandArgument(Exception):
pass
......@@ -49,6 +51,9 @@ class Command(object):
self.error = p_err
self.prompt = p_prompt
# make pretty printer available
self.printer = PrettyPrinter()
def execute(self):
"""
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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -45,6 +45,7 @@ class _Config:
'identifiers': 'linenumber',
# ls
'hide_tags': 'id,p,ical',
'indent': 0,
'list_limit': '-1',
......@@ -60,6 +61,7 @@ class _Config:
# dep
'append_parent_projects': '0',
'append_parent_contexts': '0',
}
self.config = {}
......@@ -147,6 +149,12 @@ class _Config:
except ValueError:
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):
try:
return self.config[p_tag]
......@@ -164,6 +172,11 @@ class _Config:
def tag_star(self):
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):
"""
Retrieve the config instance.
......
......@@ -16,11 +16,12 @@
import re
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinter import pretty_print, pretty_print_list
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
class DCommand(Command):
class DCommand(MultiCommand):
"""
A common class for the 'do' and 'del' operations, because they're quite
alike.
......@@ -37,13 +38,7 @@ class DCommand(Command):
self.process_flags()
self.length = len(self.todolist.todos()) # to determine newly activated todos
self.todos = []
for number in self.args:
try:
self.todos.append(self.todolist.todo(number))
except InvalidTodoException:
self.todos.append(None)
self.get_todos(self.args)
def get_flags(self):
""" Default implementation of getting specific flags. """
......@@ -72,8 +67,9 @@ class DCommand(Command):
)
def _print_list(self, p_todos):
filters = [self.todolist.pp_number()]
self.out("\n".join(pretty_print_list(p_todos, filters)))
printer = PrettyPrinter()
printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(printer.print_list(p_todos))
def prompt_text(self):
return "Yes or no? [y/N] "
......@@ -93,7 +89,7 @@ class DCommand(Command):
if not self.force and re.match('^y(es)?$', confirmation, re.I):
for child in children:
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):
delta = [todo for todo in p_new if todo not in p_old]
......@@ -133,24 +129,15 @@ class DCommand(Command):
"""
pass
def execute(self):
if not super(DCommand, self).execute():
return False
if len(self.args) == 0:
self.error(self.usage())
else:
old_active = self._active_todos()
def execute_multi_specific(self):
old_active = self._active_todos()
for todo in self.todos:
if not todo:
self.error("Invalid todo number given.")
elif todo and self.condition(todo):
self._process_subtasks(todo)
self.execute_specific(todo)
else:
self.error(self.condition_failed_text())
current_active = self._active_todos()
self._print_unlocked_todos(old_active, current_active)
for todo in self.todos:
if todo and self.condition(todo):
self._process_subtasks(todo)
self.execute_specific(todo)
else:
self.error(self.condition_failed_text())
current_active = self._active_todos()
self._print_unlocked_todos(old_active, current_active)
......@@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import pretty_print
class DeleteCommand(DCommand):
def __init__(self, p_args, p_todolist,
......@@ -35,7 +34,7 @@ class DeleteCommand(DCommand):
self.todolist.delete(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)
def usage(self):
......
......@@ -97,7 +97,8 @@ class DepCommand(Command):
if todos:
sorter = Sorter(config().sort_string())
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())
except InvalidTodoException:
self.error("Invalid todo number given.")
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -14,11 +14,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinter import pretty_print
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
class DepriCommand(Command):
class DepriCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
......@@ -26,28 +25,23 @@ class DepriCommand(Command):
super(DepriCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self):
if not super(DepriCommand, self).execute():
return False
self.get_todos(self.args)
todo = None
def execute_multi_specific(self):
try:
todo = self.todolist.todo(self.argument(0))
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if todo.priority() != None:
self.todolist.set_priority(todo, None)
self.out("Priority removed.")
self.out(pretty_print(todo))
except InvalidCommandArgument:
for todo in self.todos:
if todo.priority() != None:
self.todolist.set_priority(todo, None)
self.out("Priority removed.")
self.out(self.printer.print_todo(todo))
except IndexError:
self.error(self.usage())
except (InvalidTodoException):
if not todo:
self.error( "Invalid todo number given.")
else:
self.error(self.usage())
def usage(self):
return """Synopsis: depri <NUMBER>"""
return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]"""
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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -17,7 +17,8 @@
from datetime import date
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.Utils import date_string_to_date
......@@ -57,7 +58,10 @@ class DoCommand(DCommand):
self.completion_date)
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:
self.error("Warning: todo item has an invalid recurrence pattern.")
......@@ -80,7 +84,9 @@ class DoCommand(DCommand):
""" Actions specific to this command. """
self._handle_recurrence(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):
"""
......
......@@ -15,31 +15,152 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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.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):
super(EditCommand, self).__init__(p_args, p_todolist, p_output,
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):
if not super(EditCommand, self).execute():
if not super(ListCommand, self).execute():
return False
editor = os.environ['EDITOR'] or 'vi'
todo = config().todotxt()
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
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):
return """Synopsis: edit"""
return """Synopsis:
edit
edit <NUMBER1> [<NUMBER2> ...]
edit -e [-x] [expression]
edit -d"""
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
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):
return operand1 != operand2
return False
......@@ -20,7 +20,7 @@ value of each item.
"""
_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
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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -19,8 +19,12 @@ import re
from topydo.lib.Command import Command
from topydo.lib.Config import config
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.View import View
class ListCommand(Command):
def __init__(self, p_args, p_todolist,
......@@ -74,17 +78,29 @@ class ListCommand(Command):
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):
if not super(ListCommand, self).execute():
return False
self._process_flags()
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))
self._print()
def usage(self):
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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -16,14 +16,15 @@
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.PrettyPrinter import pretty_print
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import date_string_to_date
class PostponeCommand(Command):
class PostponeCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
......@@ -32,6 +33,8 @@ class PostponeCommand(Command):
p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False
self._process_flags()
self.get_todos(self.args[:-1])
def _process_flags(self):
opts, args = self.getopt('s')
......@@ -42,7 +45,7 @@ class PostponeCommand(Command):
self.args = args
def execute(self):
def execute_multi_specific(self):
def _get_offset(p_todo):
offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat())
......@@ -53,45 +56,38 @@ class PostponeCommand(Command):
return offset_date
if not super(PostponeCommand, self).execute():
return False
self._process_flags()
try:
todo = self.todolist.todo(self.argument(0))
pattern = self.argument(1)
# pdb.set_trace()
offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset)
if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
todo.set_tag(config().tag_start(), new_start.isoformat())
todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty()
self.out(pretty_print(todo, [self.todolist.pp_number()]))
else:
self.error("Invalid date pattern given.")
except InvalidCommandArgument:
pattern = self.args[-1]
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos:
offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset)
if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
todo.set_tag(config().tag_start(), new_start.isoformat())
todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty()
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid date pattern given.")
break
except (InvalidCommandArgument, IndexError):
self.error(self.usage())
except (InvalidTodoException):
self.error("Invalid todo number given.")
def usage(self):
return "Synopsis: postpone [-s] <NUMBER> <PATTERN>"
return "Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
def help(self):
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.
The pattern is a relative date, written in the format <COUNT><PERIOD> where
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -14,75 +14,52 @@
# 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 function to pretty print a list of todo items. """
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):
class Printer(object):
"""
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.
"""
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
def pp_indent(p_indent=0):
return lambda s, t: ' ' * p_indent + s
def pretty_print(p_todo, p_filters=None):
def print_todo(self, p_todo):
""" Base implementation. Simply returns the string conversion. """
return str(p_todo)
def print_list(self, p_todos):
"""
Given a list of todo items, pretty print it and return a list of
formatted strings.
"""
return "\n".join([self.print_todo(todo) for todo in p_todos])
class PrettyPrinter(Printer):
"""
Given a todo item, pretty print it and return a list of formatted strings.
p_filters is a list of functions that transform the output string, each
function accepting two arguments:
Prints todo items on a single line, decorated by the filters passed by
the caller.
* the todo's text that has to be modified;
* the todo object itself which allows for obtaining relevant information.
Example is pp_color in this fle.
The caller can adjust the output by passing on a set of filters, that may
add colors, indentation, etc. These filters are found in the
PrettyPrinterFilter module.
"""
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:
todo_str = f(todo_str, p_todo)
p_filter is an instance of a PrettyPrinterFilter.
"""
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):
"""
Given a list of todo items, pretty print it and return a list of
formatted strings.
"""
p_filters = p_filters or []
return [pretty_print(todo, p_filters) for todo in p_todos]
for ppf in self.filters:
todo_str = ppf.filter(todo_str, p_todo)
return todo_str
# 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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -14,12 +14,12 @@
# 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, InvalidCommandArgument
from topydo.lib.PrettyPrinter import pretty_print
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import is_valid_priority
class PriorityCommand(Command):
class PriorityCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
......@@ -27,42 +27,36 @@ class PriorityCommand(Command):
super(PriorityCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self):
if not super(PriorityCommand, self).execute():
return False
self.get_todos(self.args[:-1])
number = None
def execute_multi_specific(self):
priority = None
try:
number = self.argument(0)
priority = self.argument(1)
todo = self.todolist.todo(number)
priority = self.args[-1]
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if is_valid_priority(priority):
old_priority = todo.priority()
self.todolist.set_priority(todo, priority)
for todo in self.todos:
old_priority = todo.priority()
self.todolist.set_priority(todo, priority)
if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format(
old_priority, priority))
elif not old_priority:
self.out("Priority set to {}.".format(priority))
if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format(
old_priority, priority))
elif not old_priority:
self.out("Priority set to {}.".format(priority))
self.out(pretty_print(todo))
self.out(self.printer.print_todo(todo))
else:
self.error("Invalid priority given.")
except InvalidCommandArgument:
except IndexError:
self.error(self.usage())
except (InvalidTodoException):
if number and priority:
self.error( "Invalid todo number given.")
else:
self.error(self.usage())
def usage(self):
return """Synopsis: pri <NUMBER> <PRIORITY>"""
return """Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>"""
def help(self):
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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -15,8 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.PrettyPrinter import pretty_print
class TagCommand(Command):
def __init__(self, p_args, p_todolist,
......@@ -62,7 +62,8 @@ class TagCommand(Command):
self.value = ""
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):
"""
......
......@@ -141,6 +141,15 @@ class TodoList(TodoListBase):
for project in p_from_todo.projects() - p_to_todo.projects():
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(
hash(p_from_todo), hash(p_to_todo)):
......@@ -155,6 +164,7 @@ class TodoList(TodoListBase):
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id)
self._update_parent_cache()
append_projects_to_subtodo()
append_contexts_to_subtodo()
self.dirty = True
def remove_dependency(self, p_from_todo, p_to_todo):
......
......@@ -24,7 +24,7 @@ import re
from topydo.lib.Config import config
from topydo.lib import Filter
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.View import View
......@@ -242,14 +242,6 @@ class TodoListBase(object):
except (ValueError, KeyError):
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):
# 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
......@@ -264,5 +256,6 @@ class TodoListBase(object):
self._id_todo_map[uid] = todo
def __str__(self):
return '\n'.join(pretty_print_list(self._todos))
printer = PrettyPrinter()
return printer.print_list(self._todos)
""" Version of Topydo. """
VERSION = '0.2'
VERSION = '0.3'
LICENSE = """Copyright (C) 2014 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
# 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
# it under the terms of the GNU General Public License as published by
......@@ -16,19 +16,29 @@
""" 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):
"""
A view is instantiated by a todo list, usually obtained from a todo.txt
file. Also a sorter and a list of filters should be given that is applied
to the list.
A printer can be passed, but it won't be used when pretty_print() is
called, since it will instantiate its own pretty printer instance.
"""
def __init__(self, p_sorter, p_filters, p_todolist):
def __init__(self, p_sorter, p_filters, p_todolist,
p_printer=PrettyPrinter()):
self._todolist = p_todolist
self._viewdata = []
self._sorter = p_sorter
self._filters = p_filters
self._printer = p_printer
self.update()
......@@ -45,8 +55,20 @@ class View(object):
def pretty_print(self, p_pp_filters=None):
""" Pretty prints the view. """
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):
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