Commit aab00c3c authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into prompt

Conflicts:
	setup.py
	topydo/Commands.py
parents 721dab26 954764a4
language: python
python:
- "2.7"
install:
- "pip install ."
- "pip install icalendar"
script: python setup.py test
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,7 +3,7 @@ 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",
......@@ -11,6 +11,7 @@ setup(
extras_require = {
'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit'],
'edit-cmd-tests': ['mock'],
},
entry_points= {
'console_scripts': ['topydo = topydo.cli.CLI:main'],
......
......@@ -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()
......
# 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()
......@@ -39,31 +39,31 @@ from topydo.commands.SortCommand import SortCommand
from topydo.commands.TagCommand import TagCommand
_SUBCOMMAND_MAP = {
'add': AddCommand,
'app': AppendCommand,
'append': AppendCommand,
'del': DeleteCommand,
'dep': DepCommand,
'depri': DepriCommand,
'do': DoCommand,
'edit': EditCommand,
'exit': ExitCommand, # used for the prompt
'ical': IcalCommand,
'ls': ListCommand,
'lscon': ListContextCommand,
'listcon': ListContextCommand,
'lsprj': ListProjectCommand,
'lsproj': ListProjectCommand,
'listprj': ListProjectCommand,
'listproj': ListProjectCommand,
'listproject': ListProjectCommand,
'listprojects': ListProjectCommand,
'postpone': PostponeCommand,
'pri': PriorityCommand,
'quit': ExitCommand,
'rm': DeleteCommand,
'sort': SortCommand,
'tag': TagCommand,
'add': AddCommand,
'app': AppendCommand,
'append': AppendCommand,
'del': DeleteCommand,
'dep': DepCommand,
'depri': DepriCommand,
'do': DoCommand,
'edit': EditCommand,
'exit': ExitCommand, # used for the prompt
'ical': IcalCommand,
'ls': ListCommand,
'lscon': ListContextCommand,
'listcon': ListContextCommand,
'lsprj': ListProjectCommand,
'lsproj': ListProjectCommand,
'listprj': ListProjectCommand,
'listproj': ListProjectCommand,
'listproject': ListProjectCommand,
'listprojects': ListProjectCommand,
'postpone': PostponeCommand,
'pri': PriorityCommand,
'quit': ExitCommand,
'rm': DeleteCommand,
'sort': SortCommand,
'tag': TagCommand,
}
def get_subcommand(p_args):
......
......@@ -36,6 +36,7 @@ Available commands:
* append (app)
* del (rm)
* dep
* depri
* do
* edit
* ical
......
......@@ -14,10 +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.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,
......@@ -25,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(self.printer.print_todo(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)."""
......@@ -32,12 +32,15 @@ class EditCommand(MultiCommand, ListCommand):
p_error, p_input)
self.is_expression = False
self.edit_archive = False
def _process_flags(self):
opts, args = self.getopt('xe')
opts, args = self.getopt('xed')
for opt, value in opts:
if opt == '-x':
if opt == '-d':
self.edit_archive = True
elif opt == '-x':
self.show_all = True
elif opt == '-e':
self.is_expression = True
......@@ -101,6 +104,11 @@ class EditCommand(MultiCommand, ListCommand):
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:
......@@ -136,7 +144,8 @@ class EditCommand(MultiCommand, ListCommand):
return """Synopsis:
edit
edit <NUMBER1> [<NUMBER2> ...]
edit -e [-x] [expression]"""
edit -e [-x] [expression]
edit -d"""
def help(self):
return """\
......@@ -145,7 +154,7 @@ 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.
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'.
......@@ -153,4 +162,5 @@ 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.
"""
""" 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>.
......
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