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 0.2
--- ---
* A new 'edit' subcommand to launch an editor with the configured todo.txt file. * A new `edit` subcommand to launch an editor with the configured todo.txt file.
* Introduced textual identifiers in addition to line numbers. * Introduced textual identifiers in addition to line numbers.
Line numbers are still the default, textual identifiers can be enabled with Line numbers are still the default, textual identifiers can be enabled with
the option 'identifiers = text' in the configuration file (see topydo.conf). the option `identifiers = text` in the configuration file (see topydo.conf).
The advantage of these identifiers is that they are less prone to changes when The advantage of these identifiers is that they are less prone to changes when
something changes in the todo.txt file. For example, identifiers are much more something changes in the todo.txt file. For example, identifiers are much more
likely to remain the same when completing a todo item (and archiving it). With likely to remain the same when completing a todo item (and archiving it). With
...@@ -15,7 +35,7 @@ ...@@ -15,7 +35,7 @@
Sowiński). Sowiński).
* Multiple items can be marked as complete or deleted at once. * Multiple items can be marked as complete or deleted at once.
* Added option to automatically add the projects of the parent todo item when * Added option to automatically add the projects of the parent todo item when
adding a child todo item. Enable append_parent_projects in topydo.conf. adding a child todo item. Enable `append_parent_projects` in topydo.conf.
* `topydo help` shows a list of available subcommands. Moreover, you can run * `topydo help` shows a list of available subcommands. Moreover, you can run
`topydo help <subcommand>` as well. `topydo help <subcommand>` as well.
* Let setuptools provide a `topydo` executable. * Let setuptools provide a `topydo` executable.
......
topydo topydo
====== ======
[![Build Status](https://travis-ci.org/bram85/topydo.svg?branch=master)](https://travis-ci.org/bram85/topydo) [![Join the chat at https://gitter.im/bram85/topydo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bram85/topydo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
topydo is a todo list application using the [todo.txt format][1]. It is heavily topydo is a todo list application using the [todo.txt format][1]. It is heavily
inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a
merge between the todo.txt CLI and a [number of extensions][3] that I wrote merge between the todo.txt CLI and a [number of extensions][3] that I wrote
on top of the CLI, hereafter refered to as todo.txt-tools. These extensions on top of the CLI. These extensions are:
are:
* Set **due** and **start dates**; * Set **due** and **start dates**;
* Custom sorting; * Custom sorting;
* Dealing with tags; * Dealing with tags;
* Maintain **dependencies** between todo items; * Maintain **dependencies** between todo items;
* Allow todos to **recur**; * Allow todo items to **recur**;
* Some conveniences when adding new items (e.g. adding creation date and use * Some conveniences when adding new items (e.g. adding creation date and use
**relative dates**); **relative dates**);
......
...@@ -3,7 +3,7 @@ from setuptools import setup ...@@ -3,7 +3,7 @@ from setuptools import setup
setup( setup(
name = "topydo", name = "topydo",
packages = ["topydo", "topydo.lib", "topydo.cli"], packages = ["topydo", "topydo.lib", "topydo.cli"],
version = "0.2", version = "0.3",
description = "A command-line todo list application using the todo.txt format.", description = "A command-line todo list application using the todo.txt format.",
author = "Bram Schoenmakers", author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl", author_email = "me@bramschoenmakers.nl",
...@@ -11,6 +11,7 @@ setup( ...@@ -11,6 +11,7 @@ setup(
extras_require = { extras_require = {
'ical': ['icalendar'], 'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit'], 'prompt-toolkit': ['prompt-toolkit'],
'edit-cmd-tests': ['mock'],
}, },
entry_points= { entry_points= {
'console_scripts': ['topydo = topydo.cli.CLI:main'], 'console_scripts': ['topydo = topydo.cli.CLI:main'],
......
...@@ -26,20 +26,21 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -26,20 +26,21 @@ class DepriCommandTest(CommandTest.CommandTest):
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
"(B) Baz",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
def test_set_prio1(self): def test_depri1(self):
command = DepriCommand(["1"], self.todolist, self.out, self.error) command = DepriCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None) self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.output, "Priority removed.\nFoo\n") self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio2(self): def test_depri2(self):
command = DepriCommand(["2"], self.todolist, self.out, self.error) command = DepriCommand(["2"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -48,15 +49,26 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -48,15 +49,26 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "") self.assertEquals(self.output, "")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_set_prio3(self): def test_depri3(self):
command = DepriCommand(["Foo"], self.todolist, self.out, self.error) command = DepriCommand(["Foo"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None) self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.output, "Priority removed.\nFoo\n") self.assertEquals(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_depri4(self):
command = DepriCommand(["1","Baz"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEquals(self.todolist.todo(1).priority(), None)
self.assertEquals(self.todolist.todo(3).priority(), None)
self.assertEquals(self.output, "Priority removed.\n| 1| Foo\nPriority removed.\n| 3| Baz\n")
self.assertEquals(self.errors, "")
def test_invalid1(self): def test_invalid1(self):
command = DepriCommand(["99"], self.todolist, self.out, self.error) command = DepriCommand(["99"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -65,6 +77,22 @@ class DepriCommandTest(CommandTest.CommandTest): ...@@ -65,6 +77,22 @@ class DepriCommandTest(CommandTest.CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given.\n") self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_invalid2(self):
command = DepriCommand(["99", "1"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 99.\n")
def test_invalid3(self):
command = DepriCommand(["99", "FooBar"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEquals(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: FooBar.\n")
def test_empty(self): def test_empty(self):
command = DepriCommand([], self.todolist, self.out, self.error) command = DepriCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
......
# 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 ...@@ -39,31 +39,31 @@ from topydo.commands.SortCommand import SortCommand
from topydo.commands.TagCommand import TagCommand from topydo.commands.TagCommand import TagCommand
_SUBCOMMAND_MAP = { _SUBCOMMAND_MAP = {
'add': AddCommand, 'add': AddCommand,
'app': AppendCommand, 'app': AppendCommand,
'append': AppendCommand, 'append': AppendCommand,
'del': DeleteCommand, 'del': DeleteCommand,
'dep': DepCommand, 'dep': DepCommand,
'depri': DepriCommand, 'depri': DepriCommand,
'do': DoCommand, 'do': DoCommand,
'edit': EditCommand, 'edit': EditCommand,
'exit': ExitCommand, # used for the prompt 'exit': ExitCommand, # used for the prompt
'ical': IcalCommand, 'ical': IcalCommand,
'ls': ListCommand, 'ls': ListCommand,
'lscon': ListContextCommand, 'lscon': ListContextCommand,
'listcon': ListContextCommand, 'listcon': ListContextCommand,
'lsprj': ListProjectCommand, 'lsprj': ListProjectCommand,
'lsproj': ListProjectCommand, 'lsproj': ListProjectCommand,
'listprj': ListProjectCommand, 'listprj': ListProjectCommand,
'listproj': ListProjectCommand, 'listproj': ListProjectCommand,
'listproject': ListProjectCommand, 'listproject': ListProjectCommand,
'listprojects': ListProjectCommand, 'listprojects': ListProjectCommand,
'postpone': PostponeCommand, 'postpone': PostponeCommand,
'pri': PriorityCommand, 'pri': PriorityCommand,
'quit': ExitCommand, 'quit': ExitCommand,
'rm': DeleteCommand, 'rm': DeleteCommand,
'sort': SortCommand, 'sort': SortCommand,
'tag': TagCommand, 'tag': TagCommand,
} }
def get_subcommand(p_args): def get_subcommand(p_args):
......
...@@ -36,6 +36,7 @@ Available commands: ...@@ -36,6 +36,7 @@ Available commands:
* append (app) * append (app)
* del (rm) * del (rm)
* dep * dep
* depri
* do * do
* edit * edit
* ical * ical
......
...@@ -14,10 +14,10 @@ ...@@ -14,10 +14,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
class DepriCommand(Command): class DepriCommand(MultiCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
...@@ -25,28 +25,23 @@ class DepriCommand(Command): ...@@ -25,28 +25,23 @@ class DepriCommand(Command):
super(DepriCommand, self).__init__( super(DepriCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): self.get_todos(self.args)
if not super(DepriCommand, self).execute():
return False
todo = None def execute_multi_specific(self):
try: try:
todo = self.todolist.todo(self.argument(0)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if todo.priority() != None: for todo in self.todos:
self.todolist.set_priority(todo, None) if todo.priority() != None:
self.out("Priority removed.") self.todolist.set_priority(todo, None)
self.out(self.printer.print_todo(todo)) self.out("Priority removed.")
except InvalidCommandArgument: self.out(self.printer.print_todo(todo))
except IndexError:
self.error(self.usage()) self.error(self.usage())
except (InvalidTodoException):
if not todo:
self.error( "Invalid todo number given.")
else:
self.error(self.usage())
def usage(self): def usage(self):
return """Synopsis: depri <NUMBER>""" return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]"""
def help(self): def help(self):
return """Removes the priority of the given todo item.""" return """Removes the priority of the given todo item(s)."""
...@@ -32,12 +32,15 @@ class EditCommand(MultiCommand, ListCommand): ...@@ -32,12 +32,15 @@ class EditCommand(MultiCommand, ListCommand):
p_error, p_input) p_error, p_input)
self.is_expression = False self.is_expression = False
self.edit_archive = False
def _process_flags(self): def _process_flags(self):
opts, args = self.getopt('xe') opts, args = self.getopt('xed')
for opt, value in opts: for opt, value in opts:
if opt == '-x': if opt == '-d':
self.edit_archive = True
elif opt == '-x':
self.show_all = True self.show_all = True
elif opt == '-e': elif opt == '-e':
self.is_expression = True self.is_expression = True
...@@ -101,6 +104,11 @@ class EditCommand(MultiCommand, ListCommand): ...@@ -101,6 +104,11 @@ class EditCommand(MultiCommand, ListCommand):
else: else:
self._process_flags() self._process_flags()
if self.edit_archive:
archive = config().archive()
return call([editor, archive]) == 0
if self.is_expression: if self.is_expression:
self.todos = self._view()._viewdata self.todos = self._view()._viewdata
else: else:
...@@ -136,7 +144,8 @@ class EditCommand(MultiCommand, ListCommand): ...@@ -136,7 +144,8 @@ class EditCommand(MultiCommand, ListCommand):
return """Synopsis: return """Synopsis:
edit edit
edit <NUMBER1> [<NUMBER2> ...] edit <NUMBER1> [<NUMBER2> ...]
edit -e [-x] [expression]""" edit -e [-x] [expression]
edit -d"""
def help(self): def help(self):
return """\ return """\
...@@ -145,7 +154,7 @@ Launches a text editor to edit todos. ...@@ -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 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 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 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 By default it will use $EDITOR in your environment, otherwise it will fall back
to 'vi'. to 'vi'.
...@@ -153,4 +162,5 @@ to 'vi'. ...@@ -153,4 +162,5 @@ to 'vi'.
-e : Treat the subsequent arguments as an expression. -e : Treat the subsequent arguments as an expression.
-x : Edit *all* todos matching the expression (i.e. do not filter on -x : Edit *all* todos matching the expression (i.e. do not filter on
dependencies or relevance). dependencies or relevance).
-d : Open the archive file.
""" """
""" Version of Topydo. """ """ Version of Topydo. """
VERSION = '0.2' VERSION = '0.3'
LICENSE = """Copyright (C) 2014 Bram Schoenmakers LICENSE = """Copyright (C) 2014 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
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