Commit 8b1ff41a authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into column-ui/master

Conflicts:
	README.md
	test/test_list_format.py
	topydo/cli/CLIApplicationBase.py
	topydo/lib/Config.py
	topydo/lib/Utils.py
parents d2ba87c3 4b8c8f09
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
* Dropped support for Python 2.7. * Dropped support for Python 2.7.
* Add ability to filter on creation/completion dates: * Add ability to filter on creation/completion dates:
topydo ls created:today topydo ls created:today
topydo ls completed:today topydo ls completed:today
topydo -t done.txt completed:today # if auto-archiving is set topydo -t done.txt completed:today # if auto-archiving is set
* `ls -F` supports `%P` that expands to a single space when no priority is set, * `ls -F` supports `%P` that expands to a single space when no priority is set,
in contrast to `%p` which expands to an empty string (thanks to @MinchinWeb). in contrast to `%p` which expands to an empty string (thanks to @MinchinWeb).
...@@ -41,8 +41,8 @@ for the majority of these new features. ...@@ -41,8 +41,8 @@ for the majority of these new features.
* `ls` output can be customized with a -F flag or a configuration option: * `ls` output can be customized with a -F flag or a configuration option:
[ls] [ls]
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
or `ls -F "%{(}p{)} %s %{due:}d"`. or `ls -F "%{(}p{)} %s %{due:}d"`.
......
...@@ -43,10 +43,6 @@ smoothly into topydo. ...@@ -43,10 +43,6 @@ smoothly into topydo.
exit 1 exit 1
fi fi
if ! python2 -m pylint --errors-only topydo test; then
exit 1
fi
if ! python3 -m pylint --errors-only topydo test; then if ! python3 -m pylint --errors-only topydo test; then
exit 1 exit 1
fi fi
......
...@@ -26,22 +26,24 @@ Simply install with: ...@@ -26,22 +26,24 @@ Simply install with:
pip install topydo pip install topydo
### Optional dependencies ### Dependencies
* [arrow][8] : Used to turn dates into a human readable version.
#### Optional dependencies:
* [icalendar][7] : To print your todo.txt file as an iCalendar file * [icalendar][7] : To print your todo.txt file as an iCalendar file
(not supported for Python 3.2). (not supported for PyPy3).
* [prompt-toolkit][6] : For topydo's _prompt_ mode, which offers a shell-like * [prompt-toolkit][6] : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion. interface with auto-completion.
* [arrow][8] : Used to turn dates into a human readable version.
* [urwid][12] : For topydo's _columns_ mode, a TUI with columns for * [urwid][12] : For topydo's _columns_ mode, a TUI with columns for
your todo items. your todo items.
* [arrow][8] : Used to turn dates into a human readable version.
* [backports.shutil_get_terminal_size][9] : Used to determine your terminal * [backports.shutil_get_terminal_size][9] : Used to determine your terminal
window size. This function was window size. This function was
added to the standard library in added to the standard library in
Python 3.3 and so is only Python 3.3 and so is only
required in older versions of required for PyPy3.
Python.
* [python-dateutil][10]: A dependency of *arrow*.
* [mock][11] : Used for testing. This was added to the standard * [mock][11] : Used for testing. This was added to the standard
library in Python 3.3. library in Python 3.3.
......
...@@ -20,7 +20,7 @@ from topydo.lib.Utils import escape_ansi ...@@ -20,7 +20,7 @@ from topydo.lib.Utils import escape_ansi
class CommandTest(TopydoTest): class CommandTest(TopydoTest):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CommandTest, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.output = "" self.output = ""
self.errors = "" self.errors = ""
......
...@@ -4,3 +4,4 @@ baz = FooBar ...@@ -4,3 +4,4 @@ baz = FooBar
format = ls -F "|I| x c d {(}p{)} s k" -n 25 format = ls -F "|I| x c d {(}p{)} s k" -n 25
smile = ls smile = ls
star = tag {} star 1 star = tag {} star 1
quot = lol'd
...@@ -33,7 +33,7 @@ except ImportError: ...@@ -33,7 +33,7 @@ except ImportError:
class AddCommandTest(CommandTest): class AddCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(AddCommandTest, self).setUp() super().setUp()
self.todolist = TodoList.TodoList([]) self.todolist = TodoList.TodoList([])
self.today = date.today().isoformat() self.today = date.today().isoformat()
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class AppendCommandTest(CommandTest): class AppendCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(AppendCommandTest, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
self.todolist.add("Foo") self.todolist.add("Foo")
......
...@@ -33,7 +33,7 @@ def _no_prompt(self): ...@@ -33,7 +33,7 @@ def _no_prompt(self):
class DeleteCommandTest(CommandTest): class DeleteCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DeleteCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepCommandTest(CommandTest): class DepCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DepCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepriCommandTest(CommandTest): class DepriCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DepriCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
......
...@@ -32,7 +32,7 @@ def _no_prompt(self): ...@@ -32,7 +32,7 @@ def _no_prompt(self):
class DoCommandTest(CommandTest): class DoCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DoCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
......
...@@ -33,7 +33,7 @@ except ImportError: ...@@ -33,7 +33,7 @@ except ImportError:
class EditCommandTest(CommandTest): class EditCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(EditCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1 @test", "Bar p:1 @test",
......
...@@ -301,7 +301,7 @@ class FilterTest(TopydoTest): ...@@ -301,7 +301,7 @@ class FilterTest(TopydoTest):
class OrdinalTagFilterTest(TopydoTest): class OrdinalTagFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(OrdinalTagFilterTest, self).setUp() super().setUp()
today = date.today() today = date.today()
tomorrow = today + timedelta(1) tomorrow = today + timedelta(1)
...@@ -381,7 +381,7 @@ class OrdinalTagFilterTest(TopydoTest): ...@@ -381,7 +381,7 @@ class OrdinalTagFilterTest(TopydoTest):
class CreationFilterTest(TopydoTest): class CreationFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(CreationFilterTest, self).setUp() super().setUp()
self.todo1 = "2015-12-19 With creation date." self.todo1 = "2015-12-19 With creation date."
self.todo2 = "Without creation date." self.todo2 = "Without creation date."
...@@ -415,7 +415,7 @@ class CreationFilterTest(TopydoTest): ...@@ -415,7 +415,7 @@ class CreationFilterTest(TopydoTest):
class CompletionFilterTest(TopydoTest): class CompletionFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(CompletionFilterTest, self).setUp() super().setUp()
self.todo1 = "2015-12-19 With creation date." self.todo1 = "2015-12-19 With creation date."
self.todo2 = "x 2015-12-19 2015-12-18 Without creation date." self.todo2 = "x 2015-12-19 2015-12-18 Without creation date."
...@@ -459,7 +459,7 @@ class CompletionFilterTest(TopydoTest): ...@@ -459,7 +459,7 @@ class CompletionFilterTest(TopydoTest):
class PriorityFilterTest(TopydoTest): class PriorityFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(PriorityFilterTest, self).setUp() super().setUp()
self.todo1 = "(A) Foo" self.todo1 = "(A) Foo"
self.todo2 = "(B) Bar" self.todo2 = "(B) Bar"
......
...@@ -21,9 +21,8 @@ from topydo.Commands import get_subcommand ...@@ -21,9 +21,8 @@ from topydo.Commands import get_subcommand
from topydo.commands.AddCommand import AddCommand from topydo.commands.AddCommand import AddCommand
from topydo.commands.DeleteCommand import DeleteCommand from topydo.commands.DeleteCommand import DeleteCommand
from topydo.commands.ListCommand import ListCommand from topydo.commands.ListCommand import ListCommand
from topydo.commands.ListProjectCommand import ListProjectCommand
from topydo.commands.TagCommand import TagCommand from topydo.commands.TagCommand import TagCommand
from topydo.lib.Config import config from topydo.lib.Config import config, ConfigError
class GetSubcommandTest(TopydoTest): class GetSubcommandTest(TopydoTest):
def test_normal_cmd(self): def test_normal_cmd(self):
...@@ -120,6 +119,15 @@ class GetSubcommandTest(TopydoTest): ...@@ -120,6 +119,15 @@ class GetSubcommandTest(TopydoTest):
real_cmd, final_args = get_subcommand(args) real_cmd, final_args = get_subcommand(args)
self.assertEqual(real_cmd, None) self.assertEqual(real_cmd, None)
def test_alias_quotation(self):
config("test/data/aliases.conf")
args = ["quot"]
with self.assertRaises(ConfigError) as ce:
get_subcommand(args)
self.assertEqual(str(ce.exception), 'No closing quotation')
def test_help(self): def test_help(self):
real_cmd, final_args = get_subcommand(['help', 'nonexisting']) real_cmd, final_args = get_subcommand(['help', 'nonexisting'])
self.assertFalse(real_cmd) self.assertFalse(real_cmd)
......
...@@ -22,7 +22,7 @@ from topydo.lib.Graph import DirectedGraph ...@@ -22,7 +22,7 @@ from topydo.lib.Graph import DirectedGraph
class GraphTest(TopydoTest): class GraphTest(TopydoTest):
def setUp(self): def setUp(self):
super(GraphTest, self).setUp() super().setUp()
self.graph = DirectedGraph() self.graph = DirectedGraph()
......
...@@ -35,7 +35,7 @@ except ImportError: ...@@ -35,7 +35,7 @@ except ImportError:
class ListCommandTest(CommandTest): class ListCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(ListCommandTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt") self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines']) self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
...@@ -397,7 +397,7 @@ class ListCommandTest(CommandTest): ...@@ -397,7 +397,7 @@ class ListCommandTest(CommandTest):
class ListCommandUnicodeTest(CommandTest): class ListCommandUnicodeTest(CommandTest):
def setUp(self): def setUp(self):
super(ListCommandUnicodeTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt") self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
def test_list_unicode1(self): def test_list_unicode1(self):
......
...@@ -36,7 +36,7 @@ except ImportError: ...@@ -36,7 +36,7 @@ except ImportError:
@freeze_time("2015, 11, 06") @freeze_time("2015, 11, 06")
class ListFormatTest(CommandTest): class ListFormatTest(CommandTest):
def setUp(self): def setUp(self):
super(ListFormatTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListFormat.txt") self.todolist = load_file_to_todolist("test/data/ListFormat.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines']) self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
...@@ -132,7 +132,7 @@ class ListFormatTest(CommandTest): ...@@ -132,7 +132,7 @@ class ListFormatTest(CommandTest):
command.execute() command.execute()
result = """| 1| D Bar @Context1 +Project2 (3 months ago, due a month ago, started a month ago) result = """| 1| D Bar @Context1 +Project2 (3 months ago, due a month ago, started a month ago)
| 2| Z Lorem ipsum dolorem sit amet. Red @f... lazy:bar (just now, due in 2 days, starts in a day) | 2| Z Lorem ipsum dolorem sit amet. Red @fox ... lazy:bar (today, due in 2 days, starts in a day)
| 3| C Foo @Context2 Not@Context +Project1 Not+Project (4 months ago) | 3| C Foo @Context2 Not@Context +Project1 Not+Project (4 months ago)
| 4| C Baz @Context1 +Project1 key:value | 4| C Baz @Context1 +Project1 key:value
| 5| Drink beer @ home | 5| Drink beer @ home
...@@ -183,7 +183,7 @@ x 2014-12-12 ...@@ -183,7 +183,7 @@ x 2014-12-12
command.execute() command.execute()
result = """3 months ago | a month ago | a month ago | result = """3 months ago | a month ago | a month ago |
just now | in 2 days | in a day | today | in 2 days | in a day |
4 months ago | | | 4 months ago | | |
| | | | | |
| | | | | |
...@@ -278,7 +278,7 @@ just now | in 2 days | in a day | ...@@ -278,7 +278,7 @@ just now | in 2 days | in a day |
command.execute() command.execute()
result = """3 months ago result = """3 months ago
just now today
4 months ago 4 months ago
...@@ -330,7 +330,7 @@ due in 2 days, starts in a day ...@@ -330,7 +330,7 @@ due in 2 days, starts in a day
command.execute() command.execute()
result = """3 months ago, due a month ago, started a month ago result = """3 months ago, due a month ago, started a month ago
just now, due in 2 days, starts in a day today, due in 2 days, starts in a day
4 months ago 4 months ago
...@@ -697,5 +697,19 @@ C - ...@@ -697,5 +697,19 @@ C -
""" """
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
def test_list_format46(self):
command = ListCommand(["-x", "-F", "%r"], self.todolist, self.out, self.error)
command.execute()
result = """(D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
(Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the lazy:bar and jar due:2015-11-08 t:2015-11-07
(C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
(C) Baz @Context1 +Project1 key:value
Drink beer @ home id:1 p:2 ical:foobar
x 2014-12-12 Completed but with date:2014-12-12
"""
self.assertEqual(self.output, result)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList ...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class PostponeCommandTest(CommandTest): class PostponeCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(PostponeCommandTest, self).setUp() super().setUp()
self.today = date.today() self.today = date.today()
self.past = date.today() - timedelta(1) self.past = date.today() - timedelta(1)
self.future = date.today() + timedelta(1) self.future = date.today() + timedelta(1)
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class PriorityCommandTest(CommandTest): class PriorityCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(PriorityCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
......
...@@ -25,7 +25,7 @@ from topydo.lib.Todo import Todo ...@@ -25,7 +25,7 @@ from topydo.lib.Todo import Todo
class RecurrenceTest(TopydoTest): class RecurrenceTest(TopydoTest):
def setUp(self): def setUp(self):
super(RecurrenceTest, self).setUp() super().setUp()
self.todo = Todo("Test rec:1w") self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w") self.stricttodo = Todo("Test rec:+1w")
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest import unittest
from datetime import date, timedelta from datetime import date
from freezegun import freeze_time from freezegun import freeze_time
from test.topydo_testcase import TopydoTest from test.topydo_testcase import TopydoTest
...@@ -25,7 +25,7 @@ from topydo.lib.RelativeDate import relative_date_to_date ...@@ -25,7 +25,7 @@ from topydo.lib.RelativeDate import relative_date_to_date
@freeze_time('2015, 11, 06') @freeze_time('2015, 11, 06')
class RelativeDateTester(TopydoTest): class RelativeDateTester(TopydoTest):
def setUp(self): def setUp(self):
super(RelativeDateTester, self).setUp() super().setUp()
self.yesterday = date(2015, 11, 5) self.yesterday = date(2015, 11, 5)
self.today = date(2015, 11, 6) self.today = date(2015, 11, 6)
self.tomorrow = date(2015, 11, 7) self.tomorrow = date(2015, 11, 7)
...@@ -40,6 +40,18 @@ class RelativeDateTester(TopydoTest): ...@@ -40,6 +40,18 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('1d') result = relative_date_to_date('1d')
self.assertEqual(result, self.tomorrow) self.assertEqual(result, self.tomorrow)
def test_zero_bdays(self):
result = relative_date_to_date('0b')
self.assertEqual(result, self.today)
def test_one_bday(self):
result = relative_date_to_date('1b')
self.assertEqual(result, self.monday)
def test_one_bweek(self):
result = relative_date_to_date('5b')
self.assertEqual(result, self.friday)
def test_one_week(self): def test_one_week(self):
result = relative_date_to_date('1w') result = relative_date_to_date('1w')
self.assertEqual(result, date(2015, 11, 13)) self.assertEqual(result, date(2015, 11, 13))
...@@ -152,6 +164,14 @@ class RelativeDateTester(TopydoTest): ...@@ -152,6 +164,14 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('-0d') result = relative_date_to_date('-0d')
self.assertTrue(result, self.today) self.assertTrue(result, self.today)
def test_negative_period3(self):
result = relative_date_to_date('-1b')
self.assertEqual(result, date(2015, 11, 5))
def test_negative_period4(self):
result = relative_date_to_date('-5b')
self.assertEqual(result, date(2015, 10, 30))
def test_weekday_next_week(self): def test_weekday_next_week(self):
""" """
When entering "Friday" on a Friday, return next week Friday instead of When entering "Friday" on a Friday, return next week Friday instead of
......
...@@ -35,7 +35,7 @@ from topydo.lib.TodoList import TodoList ...@@ -35,7 +35,7 @@ from topydo.lib.TodoList import TodoList
class RevertCommandTest(CommandTest): class RevertCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(RevertCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo", "Foo",
"Bar", "Bar",
......
...@@ -24,7 +24,7 @@ from topydo.lib.Config import config ...@@ -24,7 +24,7 @@ from topydo.lib.Config import config
class SortCommandTest(CommandTest): class SortCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(SortCommandTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/SorterTest1.txt") self.todolist = load_file_to_todolist("test/data/SorterTest1.txt")
def test_sort1(self): def test_sort1(self):
......
...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList ...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class TagCommandTest(CommandTest): class TagCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(TagCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo", "Foo",
"Bar due:2014-10-22", "Bar due:2014-10-22",
......
...@@ -29,7 +29,7 @@ from topydo.lib.TodoListBase import InvalidTodoException, TodoListBase ...@@ -29,7 +29,7 @@ from topydo.lib.TodoListBase import InvalidTodoException, TodoListBase
class TodoListTester(TopydoTest): class TodoListTester(TopydoTest):
def setUp(self): def setUp(self):
super(TodoListTester, self).setUp() super().setUp()
self.todofile = TodoFile('test/data/TodoListTest.txt') self.todofile = TodoFile('test/data/TodoListTest.txt')
lines = [line for line in self.todofile.read() lines = [line for line in self.todofile.read()
...@@ -219,6 +219,13 @@ class TodoListTester(TopydoTest): ...@@ -219,6 +219,13 @@ class TodoListTester(TopydoTest):
config("test/data/todolist-uid.conf") config("test/data/todolist-uid.conf")
self.assertRaises(InvalidTodoException, self.todolist.todo, 1) self.assertRaises(InvalidTodoException, self.todolist.todo, 1)
def test_uid4(self):
"""
Handle UIDs properly when line numbers are configured.
"""
config(p_overrides={('topydo', 'identifiers'): 'linenumber'})
self.assertRaises(InvalidTodoException, self.todolist.todo, '11a')
def test_new_uid(self): def test_new_uid(self):
""" Make sure that item has new text ID after append. """ """ Make sure that item has new text ID after append. """
config("test/data/todolist-uid.conf") config("test/data/todolist-uid.conf")
...@@ -227,10 +234,23 @@ class TodoListTester(TopydoTest): ...@@ -227,10 +234,23 @@ class TodoListTester(TopydoTest):
self.assertNotEqual(self.todolist.number(todo), 't5c') self.assertNotEqual(self.todolist.number(todo), 't5c')
def test_iteration(self):
""" Confirms that the iternation method is working. """
results = ["(C) Foo @Context2 Not@Context +Project1 Not+Project",
"(D) Bar @Context1 +Project2",
"(C) Baz @Context1 +Project1 key:value",
"(C) Drink beer @ home",
"(C) 13 + 29 = 42"]
i = 0
for todo in self.todolist:
self.assertEqual(todo.src, results[i])
i += 1
class TodoListDependencyTester(TopydoTest): class TodoListDependencyTester(TopydoTest):
def setUp(self): def setUp(self):
super(TodoListDependencyTester, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
self.todolist.add("Foo id:1") self.todolist.add("Foo id:1")
...@@ -242,6 +262,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -242,6 +262,7 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add("Another one with +Project") self.todolist.add("Another one with +Project")
self.todolist.add("Todo with +AnotherProject") self.todolist.add("Todo with +AnotherProject")
self.todolist.add("Todo without children id:3") self.todolist.add("Todo without children id:3")
self.todolist.add("Orphan p:4")
def test_check_dep(self): def test_check_dep(self):
children = self.todolist.children(self.todolist.todo(1)) children = self.todolist.children(self.todolist.todo(1))
...@@ -272,8 +293,8 @@ class TodoListDependencyTester(TopydoTest): ...@@ -272,8 +293,8 @@ class TodoListDependencyTester(TopydoTest):
todo5 = self.todolist.todo(5) todo5 = self.todolist.todo(5)
self.todolist.add_dependency(todo5, todo4) self.todolist.add_dependency(todo5, todo4)
self.assertTrue(todo5.has_tag('id', '4')) self.assertTrue(todo5.has_tag('id', '5'))
self.assertTrue(todo4.has_tag('p', '4')) self.assertTrue(todo4.has_tag('p', '5'))
def test_add_dep2(self): def test_add_dep2(self):
""" """
...@@ -287,8 +308,8 @@ class TodoListDependencyTester(TopydoTest): ...@@ -287,8 +308,8 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add_dependency(todo5, todo4) self.todolist.add_dependency(todo5, todo4)
self.todolist.add_dependency(todo4, todo1) self.todolist.add_dependency(todo4, todo1)
self.assertTrue(todo4.has_tag('id', '5')) self.assertTrue(todo4.has_tag('id', '6'))
self.assertTrue(todo1.has_tag('p', '5')) self.assertTrue(todo1.has_tag('p', '6'))
def test_add_dep3(self): def test_add_dep3(self):
""" """
...@@ -322,6 +343,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -322,6 +343,7 @@ class TodoListDependencyTester(TopydoTest):
self.assertFalse(from_todo.has_tag('id')) self.assertFalse(from_todo.has_tag('id'))
self.assertFalse(to_todo.has_tag('p')) self.assertFalse(to_todo.has_tag('p'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
def test_remove_dep2(self): def test_remove_dep2(self):
old = str(self.todolist) old = str(self.todolist)
...@@ -330,6 +352,9 @@ class TodoListDependencyTester(TopydoTest): ...@@ -330,6 +352,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo) self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old) self.assertEqual(str(self.todolist), old)
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertTrue(self.todolist.todo_by_dep_id('2'))
self.assertTrue(self.todolist.todo_by_dep_id('3'))
def test_remove_dep3(self): def test_remove_dep3(self):
""" Try to remove non-existing dependency. """ """ Try to remove non-existing dependency. """
...@@ -339,6 +364,9 @@ class TodoListDependencyTester(TopydoTest): ...@@ -339,6 +364,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo) self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old) self.assertEqual(str(self.todolist), old)
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertTrue(self.todolist.todo_by_dep_id('2'))
self.assertTrue(self.todolist.todo_by_dep_id('3'))
def test_remove_todo_check_children(self): def test_remove_todo_check_children(self):
todo = self.todolist.todo(2) todo = self.todolist.todo(2)
...@@ -351,6 +379,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -351,6 +379,7 @@ class TodoListDependencyTester(TopydoTest):
todo = self.todolist.todo(3) todo = self.todolist.todo(3)
self.todolist.delete(todo) self.todolist.delete(todo)
self.assertFalse(todo.has_tag('p', '2')) self.assertFalse(todo.has_tag('p', '2'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
todo = self.todolist.todo(1) todo = self.todolist.todo(1)
children = self.todolist.children(todo) children = self.todolist.children(todo)
...@@ -372,6 +401,20 @@ class TodoListDependencyTester(TopydoTest): ...@@ -372,6 +401,20 @@ class TodoListDependencyTester(TopydoTest):
self.assertTrue(todolist.todo_by_dep_id('1')) self.assertTrue(todolist.todo_by_dep_id('1'))
self.assertFalse(todolist.todo_by_dep_id('2')) self.assertFalse(todolist.todo_by_dep_id('2'))
def test_add_after_dependencies(self):
"""
Test that information is properly stored after dependency related
information was retrieved from the todo list.
"""
todo = self.todolist.todo(1)
self.todolist.parents(todo)
self.todolist.add('New dependency id:99')
self.todolist.add('Child p:99')
self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo_by_dep_id('99'))
class TodoListCleanDependencyTester(TopydoTest): class TodoListCleanDependencyTester(TopydoTest):
""" """
...@@ -383,7 +426,7 @@ class TodoListCleanDependencyTester(TopydoTest): ...@@ -383,7 +426,7 @@ class TodoListCleanDependencyTester(TopydoTest):
""" """
def setUp(self): def setUp(self):
super(TodoListCleanDependencyTester, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
def test_clean_dependencies1(self): def test_clean_dependencies1(self):
...@@ -419,19 +462,7 @@ class TodoListCleanDependencyTester(TopydoTest): ...@@ -419,19 +462,7 @@ class TodoListCleanDependencyTester(TopydoTest):
self.todolist.clean_dependencies() self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(1).has_tag('id')) self.assertFalse(self.todolist.todo(1).has_tag('id'))
self.assertFalse(self.todolist.todo_by_dep_id('1'))
def test_clean_dependencies4(self):
""" Clean p: items when siblings are still connected to parent. """
self.todolist.add("Foo id:1")
self.todolist.add("Bar p:1")
self.todolist.add("Baz p:1 id:2")
self.todolist.add("Buzz p:2 p:1")
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(4).has_tag('p', '1'))
self.assertTrue(self.todolist.todo(1).has_tag('id', '1'))
self.assertTrue(self.todolist.todo(2).has_tag('p', '1'))
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -21,7 +21,7 @@ instance based on an argument list. ...@@ -21,7 +21,7 @@ instance based on an argument list.
import sys import sys
from topydo.lib.Config import config from topydo.lib.Config import config, ConfigError
_SUBCOMMAND_MAP = { _SUBCOMMAND_MAP = {
'add': 'AddCommand', 'add': 'AddCommand',
...@@ -90,7 +90,11 @@ def get_subcommand(p_args): ...@@ -90,7 +90,11 @@ def get_subcommand(p_args):
If alias resolves to non-existent command, main help message is If alias resolves to non-existent command, main help message is
returned. returned.
""" """
real_subcommand, alias_args = alias_map[p_alias] try:
real_subcommand, alias_args = alias_map[p_alias]
except ValueError as ve:
raise ConfigError(alias_map[p_alias]) from ve
try: try:
result = import_subcommand(real_subcommand) result = import_subcommand(real_subcommand)
args = join_args(p_args, alias_args) args = join_args(p_args, alias_args)
......
...@@ -41,7 +41,7 @@ class CLIApplication(CLIApplicationBase): ...@@ -41,7 +41,7 @@ class CLIApplication(CLIApplicationBase):
""" """
def __init__(self): def __init__(self):
super(CLIApplication, self).__init__() super().__init__()
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
...@@ -50,7 +50,11 @@ class CLIApplication(CLIApplicationBase): ...@@ -50,7 +50,11 @@ class CLIApplication(CLIApplicationBase):
self.todofile = TodoFile.TodoFile(config().todotxt()) self.todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(self.todofile.read()) self.todolist = TodoList.TodoList(self.todofile.read())
(subcommand, args) = get_subcommand(args) try:
(subcommand, args) = get_subcommand(args)
except ConfigError as ce:
error('Error: ' + str(ce) + '. Check your aliases configuration')
sys.exit(1)
if subcommand is None: if subcommand is None:
self._usage() self._usage()
......
...@@ -226,9 +226,9 @@ class CLIApplicationBase(object): ...@@ -226,9 +226,9 @@ class CLIApplicationBase(object):
to the todo.txt file. to the todo.txt file.
""" """
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.todolist.dirty: if self.todolist.dirty:
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.do_archive and config().archive(): if self.do_archive and config().archive():
self._archive() self._archive()
......
...@@ -59,7 +59,7 @@ class PromptApplication(CLIApplicationBase): ...@@ -59,7 +59,7 @@ class PromptApplication(CLIApplicationBase):
""" """
def __init__(self): def __init__(self):
super(PromptApplication, self).__init__() super().__init__()
self._process_flags() self._process_flags()
self.mtime = None self.mtime = None
...@@ -95,11 +95,20 @@ class PromptApplication(CLIApplicationBase): ...@@ -95,11 +95,20 @@ class PromptApplication(CLIApplicationBase):
completer=self.completer, completer=self.completer,
complete_while_typing=False) complete_while_typing=False)
user_input = shlex.split(user_input) user_input = shlex.split(user_input)
except (EOFError, KeyboardInterrupt): except EOFError:
sys.exit(0) sys.exit(0)
except KeyboardInterrupt:
continue
except ValueError as verr:
error('Error: ' + str(verr))
mtime_after = _todotxt_mtime() mtime_after = _todotxt_mtime()
(subcommand, args) = get_subcommand(user_input)
try:
(subcommand, args) = get_subcommand(user_input)
except ConfigError as ce:
error('Error: ' + str(ce) + '. Check your aliases configuration')
continue
# refuse to perform operations such as 'del' and 'do' if the # refuse to perform operations such as 'del' and 'do' if the
# todo.txt file has been changed in the background. # todo.txt file has been changed in the background.
......
...@@ -34,7 +34,7 @@ class AddCommand(Command): ...@@ -34,7 +34,7 @@ class AddCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(AddCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.text = ' '.join(p_args) self.text = ' '.join(p_args)
self.from_file = None self.from_file = None
...@@ -119,7 +119,7 @@ class AddCommand(Command): ...@@ -119,7 +119,7 @@ class AddCommand(Command):
def execute(self): def execute(self):
""" Adds a todo item to the list. """ """ Adds a todo item to the list. """
if not super(AddCommand, self).execute(): if not super().execute():
return False return False
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
......
...@@ -24,11 +24,11 @@ class AppendCommand(Command): ...@@ -24,11 +24,11 @@ class AppendCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(AppendCommand, self).__init__(p_args, p_todolist, p_out, p_err, super().__init__(p_args, p_todolist, p_out, p_err,
p_prompt) p_prompt)
def execute(self): def execute(self):
if not super(AppendCommand, self).execute(): if not super().execute():
return False return False
try: try:
......
...@@ -27,7 +27,7 @@ class ArchiveCommand(Command): ...@@ -27,7 +27,7 @@ class ArchiveCommand(Command):
TodoListBase class which does no dependency checking, so a better TodoListBase class which does no dependency checking, so a better
choice for huge done.txt files. choice for huge done.txt files.
""" """
super(ArchiveCommand, self).__init__([], p_todolist) super().__init__([], p_todolist)
self.archive = p_archive_list self.archive = p_archive_list
def execute(self): def execute(self):
......
...@@ -22,7 +22,7 @@ class DeleteCommand(DCommand): ...@@ -22,7 +22,7 @@ class DeleteCommand(DCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DeleteCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def prompt_text(self): def prompt_text(self):
......
...@@ -28,7 +28,7 @@ class DepCommand(Command): ...@@ -28,7 +28,7 @@ class DepCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DepCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
try: try:
...@@ -109,7 +109,7 @@ class DepCommand(Command): ...@@ -109,7 +109,7 @@ class DepCommand(Command):
self.error(self.usage()) self.error(self.usage())
def execute(self): def execute(self):
if not super(DepCommand, self).execute(): if not super().execute():
return False return False
dispatch = { dispatch = {
......
...@@ -23,7 +23,7 @@ class DepriCommand(MultiCommand): ...@@ -23,7 +23,7 @@ class DepriCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DepriCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def _execute_multi_specific(self): def _execute_multi_specific(self):
......
...@@ -33,17 +33,17 @@ class DoCommand(DCommand): ...@@ -33,17 +33,17 @@ class DoCommand(DCommand):
self.strict_recurrence = False self.strict_recurrence = False
self.completion_date = date.today() self.completion_date = date.today()
super(DoCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def get_flags(self): def get_flags(self):
""" Additional flags. """ """ Additional flags. """
opts, long_opts = super(DoCommand, self).get_flags() opts, long_opts = super().get_flags()
return ("d:s" + opts, ["date=", "strict"] + long_opts) return ("d:s" + opts, ["date=", "strict"] + long_opts)
def process_flag(self, p_opt, p_value): def process_flag(self, p_opt, p_value):
super(DoCommand, self).process_flag(p_opt, p_value) super().process_flag(p_opt, p_value)
if p_opt == "-s" or p_opt == "--strict": if p_opt == "-s" or p_opt == "--strict":
self.strict_recurrence = True self.strict_recurrence = True
......
...@@ -39,7 +39,7 @@ def _is_edited(p_orig_mtime, p_file): ...@@ -39,7 +39,7 @@ def _is_edited(p_orig_mtime, p_file):
class EditCommand(MultiCommand): class EditCommand(MultiCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(EditCommand, self).__init__(p_args, p_todolist, p_output, super().__init__(p_args, p_todolist, p_output,
p_error, p_input) p_error, p_input)
if len(self.args) == 0: if len(self.args) == 0:
......
...@@ -26,11 +26,11 @@ class ExitCommand(Command): ...@@ -26,11 +26,11 @@ class ExitCommand(Command):
""" """
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(ExitCommand, self).__init__(p_args, p_todolist, p_output, p_error, super().__init__(p_args, p_todolist, p_output, p_error,
p_input) p_input)
def execute(self): def execute(self):
if not super(ExitCommand, self).execute(): if not super().execute():
return False return False
sys.exit(0) sys.exit(0)
...@@ -28,7 +28,7 @@ class ListCommand(ExpressionCommand): ...@@ -28,7 +28,7 @@ class ListCommand(ExpressionCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = None self.printer = None
...@@ -92,7 +92,7 @@ class ListCommand(ExpressionCommand): ...@@ -92,7 +92,7 @@ class ListCommand(ExpressionCommand):
Additional filters to select particular todo items given with the -i Additional filters to select particular todo items given with the -i
flag. flag.
""" """
filters = super(ListCommand, self)._filters() filters = super()._filters()
if self.ids: if self.ids:
def get_todo(p_id): def get_todo(p_id):
...@@ -122,7 +122,6 @@ class ListCommand(ExpressionCommand): ...@@ -122,7 +122,6 @@ class ListCommand(ExpressionCommand):
# create a standard printer with some filters # create a standard printer with some filters
indent = config().list_indent() indent = config().list_indent()
final_format = ' ' * indent + self.format final_format = ' ' * indent + self.format
hidden_tags = config().hidden_tags()
filters = [] filters = []
filters.append(PrettyPrinterFormatFilter(self.todolist, final_format)) filters.append(PrettyPrinterFormatFilter(self.todolist, final_format))
...@@ -132,7 +131,7 @@ class ListCommand(ExpressionCommand): ...@@ -132,7 +131,7 @@ class ListCommand(ExpressionCommand):
self.out(self.printer.print_list(self._view().todos)) self.out(self.printer.print_list(self._view().todos))
def execute(self): def execute(self):
if not super(ListCommand, self).execute(): if not super().execute():
return False return False
try: try:
......
...@@ -22,11 +22,11 @@ class ListContextCommand(Command): ...@@ -22,11 +22,11 @@ class ListContextCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListContextCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(ListContextCommand, self).execute(): if not super().execute():
return False return False
for context in sorted(self.todolist.contexts(), key=lambda s: s.lower()): for context in sorted(self.todolist.contexts(), key=lambda s: s.lower()):
......
...@@ -22,11 +22,11 @@ class ListProjectCommand(Command): ...@@ -22,11 +22,11 @@ class ListProjectCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListProjectCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(ListProjectCommand, self).execute(): if not super().execute():
return False return False
for project in sorted(self.todolist.projects(), key=lambda s: s.lower()): for project in sorted(self.todolist.projects(), key=lambda s: s.lower()):
......
...@@ -28,7 +28,7 @@ class PostponeCommand(MultiCommand): ...@@ -28,7 +28,7 @@ class PostponeCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(PostponeCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False self.move_start_date = False
......
...@@ -26,7 +26,7 @@ class PriorityCommand(MultiCommand): ...@@ -26,7 +26,7 @@ class PriorityCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(PriorityCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.last_argument = True self.last_argument = True
......
...@@ -25,11 +25,11 @@ class RevertCommand(Command): ...@@ -25,11 +25,11 @@ class RevertCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(RevertCommand, self).__init__(p_args, p_todolist, p_out, p_err, super().__init__(p_args, p_todolist, p_out, p_err,
p_prompt) p_prompt)
def execute(self): def execute(self):
if not super(RevertCommand, self).execute(): if not super().execute():
return False return False
archive_file = TodoFile.TodoFile(config().archive()) archive_file = TodoFile.TodoFile(config().archive())
......
...@@ -24,11 +24,11 @@ class SortCommand(Command): ...@@ -24,11 +24,11 @@ class SortCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(SortCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(SortCommand, self).execute(): if not super().execute():
return False return False
try: try:
......
...@@ -26,7 +26,7 @@ class TagCommand(Command): ...@@ -26,7 +26,7 @@ class TagCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(TagCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False self.force = False
...@@ -124,7 +124,7 @@ class TagCommand(Command): ...@@ -124,7 +124,7 @@ class TagCommand(Command):
self._print() self._print()
def execute(self): def execute(self):
if not super(TagCommand, self).execute(): if not super().execute():
return False return False
self._process_args() self._process_args()
......
...@@ -71,8 +71,8 @@ class Command(object): ...@@ -71,8 +71,8 @@ class Command(object):
""" Retrieves a value from the argument list at the given position. """ """ Retrieves a value from the argument list at the given position. """
try: try:
return self.args[p_number] return self.args[p_number]
except IndexError: except IndexError as ie:
raise InvalidCommandArgument raise InvalidCommandArgument from ie
def getopt(self, p_flags, p_long=None): def getopt(self, p_flags, p_long=None):
p_long = p_long or [] p_long = p_long or []
......
...@@ -354,10 +354,13 @@ class _Config: ...@@ -354,10 +354,13 @@ class _Config:
alias_dict = dict() alias_dict = dict()
for alias, meaning in aliases: for alias, meaning in aliases:
meaning = shlex.split(meaning) try:
real_subcommand = meaning[0] meaning = shlex.split(meaning)
alias_args = meaning[1:] real_subcommand = meaning[0]
alias_dict[alias] = (real_subcommand, alias_args) alias_args = meaning[1:]
alias_dict[alias] = (real_subcommand, alias_args)
except ValueError as verr:
alias_dict[alias] = str(verr)
return alias_dict return alias_dict
...@@ -404,7 +407,7 @@ def config(p_path=None, p_overrides=None): ...@@ -404,7 +407,7 @@ def config(p_path=None, p_overrides=None):
try: try:
config.instance = _Config(p_path, p_overrides) config.instance = _Config(p_path, p_overrides)
except configparser.ParsingError as perr: except configparser.ParsingError as perr:
raise ConfigError(str(perr)) raise ConfigError(str(perr)) from perr
return config.instance return config.instance
......
...@@ -31,7 +31,7 @@ class DCommand(MultiCommand): ...@@ -31,7 +31,7 @@ class DCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False self.force = False
......
...@@ -32,7 +32,7 @@ class ExpressionCommand(Command): ...@@ -32,7 +32,7 @@ class ExpressionCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ExpressionCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.sort_expression = config().sort_string() self.sort_expression = config().sort_string()
......
...@@ -62,7 +62,7 @@ class GrepFilter(Filter): ...@@ -62,7 +62,7 @@ class GrepFilter(Filter):
""" Matches when the todo text contains a text. """ """ Matches when the todo text contains a text. """
def __init__(self, p_expression, p_case_sensitive=None): def __init__(self, p_expression, p_case_sensitive=None):
super(GrepFilter, self).__init__() super().__init__()
# convert to string in case we receive integers # convert to string in case we receive integers
self.expression = p_expression self.expression = p_expression
...@@ -96,13 +96,15 @@ class RelevanceFilter(Filter): ...@@ -96,13 +96,15 @@ class RelevanceFilter(Filter):
""" """
def match(self, p_todo): def match(self, p_todo):
is_due = p_todo.is_active() active = p_todo.is_active()
is_due |= p_todo.due_date() == None
is_due |= p_todo.priority() == 'A'
is_due |= p_todo.priority() == 'B' and p_todo.days_till_due() <= 30
is_due |= p_todo.priority() == 'C' and p_todo.days_till_due() <= 14
return p_todo.is_active() and is_due is_due = active
is_due = is_due or p_todo.due_date() == None
is_due = is_due or p_todo.priority() == 'A'
is_due = is_due or (p_todo.priority() == 'B' and p_todo.days_till_due() <= 30)
is_due = is_due or (p_todo.priority() == 'C' and p_todo.days_till_due() <= 14)
return active and is_due
class DependencyFilter(Filter): class DependencyFilter(Filter):
...@@ -115,7 +117,7 @@ class DependencyFilter(Filter): ...@@ -115,7 +117,7 @@ class DependencyFilter(Filter):
Pass on a TodoList instance such that the dependencies can be Pass on a TodoList instance such that the dependencies can be
looked up. looked up.
""" """
super(DependencyFilter, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
def match(self, p_todo): def match(self, p_todo):
...@@ -138,7 +140,7 @@ class InstanceFilter(Filter): ...@@ -138,7 +140,7 @@ class InstanceFilter(Filter):
This is handy for constructing a view given a plain list of Todo items. This is handy for constructing a view given a plain list of Todo items.
""" """
super(InstanceFilter, self).__init__() super().__init__()
self.todos = p_todos self.todos = p_todos
def match(self, p_todo): def match(self, p_todo):
...@@ -154,7 +156,7 @@ class InstanceFilter(Filter): ...@@ -154,7 +156,7 @@ class InstanceFilter(Filter):
class LimitFilter(Filter): class LimitFilter(Filter):
def __init__(self, p_limit): def __init__(self, p_limit):
super(LimitFilter, self).__init__() super().__init__()
self.limit = p_limit self.limit = p_limit
def filter(self, p_todos): def filter(self, p_todos):
...@@ -167,7 +169,7 @@ class OrdinalFilter(Filter): ...@@ -167,7 +169,7 @@ class OrdinalFilter(Filter):
""" Base class for ordinal filters. """ """ Base class for ordinal filters. """
def __init__(self, p_expression, p_pattern): def __init__(self, p_expression, p_pattern):
super(OrdinalFilter, self).__init__() super().__init__()
self.expression = p_expression self.expression = p_expression
...@@ -206,7 +208,7 @@ _ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):" + _OPERATOR_MATCH + _VALUE_MATCH ...@@ -206,7 +208,7 @@ _ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):" + _OPERATOR_MATCH + _VALUE_MATCH
class OrdinalTagFilter(OrdinalFilter): class OrdinalTagFilter(OrdinalFilter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(OrdinalTagFilter, self).__init__(p_expression, _ORDINAL_TAG_MATCH) super().__init__(p_expression, _ORDINAL_TAG_MATCH)
def match(self, p_todo): def match(self, p_todo):
""" """
...@@ -247,7 +249,7 @@ class OrdinalTagFilter(OrdinalFilter): ...@@ -247,7 +249,7 @@ class OrdinalTagFilter(OrdinalFilter):
class _DateAttributeFilter(OrdinalFilter): class _DateAttributeFilter(OrdinalFilter):
def __init__(self, p_expression, p_match, p_getter): def __init__(self, p_expression, p_match, p_getter):
super(_DateAttributeFilter, self).__init__(p_expression, p_match) super().__init__(p_expression, p_match)
self.getter = p_getter self.getter = p_getter
def match(self, p_todo): def match(self, p_todo):
...@@ -268,7 +270,7 @@ _CREATED_MATCH = r'creat(ion|ed?):' + _OPERATOR_MATCH + _VALUE_MATCH ...@@ -268,7 +270,7 @@ _CREATED_MATCH = r'creat(ion|ed?):' + _OPERATOR_MATCH + _VALUE_MATCH
class CreationFilter(_DateAttributeFilter): class CreationFilter(_DateAttributeFilter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(CreationFilter, self).__init__( super().__init__(
p_expression, p_expression,
_CREATED_MATCH, _CREATED_MATCH,
lambda t: t.creation_date() # pragma: no branch lambda t: t.creation_date() # pragma: no branch
...@@ -280,7 +282,7 @@ _COMPLETED_MATCH = r'complet(ed?|ion):' + _OPERATOR_MATCH + _VALUE_MATCH ...@@ -280,7 +282,7 @@ _COMPLETED_MATCH = r'complet(ed?|ion):' + _OPERATOR_MATCH + _VALUE_MATCH
class CompletionFilter(_DateAttributeFilter): class CompletionFilter(_DateAttributeFilter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(CompletionFilter, self).__init__( super().__init__(
p_expression, p_expression,
_COMPLETED_MATCH, _COMPLETED_MATCH,
lambda t: t.completion_date() # pragma: no branch lambda t: t.completion_date() # pragma: no branch
...@@ -292,7 +294,7 @@ _PRIORITY_MATCH = r"\(" + _OPERATOR_MATCH + r"(?P<value>[A-Z]{1})\)" ...@@ -292,7 +294,7 @@ _PRIORITY_MATCH = r"\(" + _OPERATOR_MATCH + r"(?P<value>[A-Z]{1})\)"
class PriorityFilter(OrdinalFilter): class PriorityFilter(OrdinalFilter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(PriorityFilter, self).__init__(p_expression, _PRIORITY_MATCH) super().__init__(p_expression, _PRIORITY_MATCH)
def match(self, p_todo): def match(self, p_todo):
""" """
......
...@@ -66,7 +66,7 @@ class IcalPrinter(Printer): ...@@ -66,7 +66,7 @@ class IcalPrinter(Printer):
""" """
def __init__(self, p_todolist): def __init__(self, p_todolist):
super(IcalPrinter, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
try: try:
......
...@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()): ...@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
average = 0 average = 0
parents = [] parents = []
if 'parents' in p_todo.attributes: try:
sum_importance = own_importance sum_importance = own_importance
parents = p_todo.attributes['parents'] parents = p_todo.parents()
for parent in parents: for parent in parents:
sum_importance += importance(parent, p_ignore_weekend) sum_importance += importance(parent, p_ignore_weekend)
average = float(sum_importance) / float(1 + len(parents)) average = float(sum_importance) / float(1 + len(parents))
except AttributeError:
pass
return max(own_importance, average) return max(own_importance, average)
...@@ -52,7 +52,7 @@ class JsonPrinter(Printer): ...@@ -52,7 +52,7 @@ class JsonPrinter(Printer):
""" """
def __init__(self): def __init__(self):
super(JsonPrinter, self).__init__() super().__init__()
def print_todo(self, p_todo): def print_todo(self, p_todo):
return json.dumps(_convert_todo(p_todo), ensure_ascii=False, return json.dumps(_convert_todo(p_todo), ensure_ascii=False,
......
...@@ -43,7 +43,7 @@ def humanize_date(p_datetime): ...@@ -43,7 +43,7 @@ def humanize_date(p_datetime):
""" Returns a relative date string from a datetime object. """ """ Returns a relative date string from a datetime object. """
now = arrow.now() now = arrow.now()
date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year) date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year)
return date.humanize() return date.humanize().replace('just now', 'today')
def humanize_dates(p_due=None, p_start=None, p_creation=None): def humanize_dates(p_due=None, p_start=None, p_creation=None):
""" """
...@@ -183,6 +183,9 @@ class ListFormatParser(object): ...@@ -183,6 +183,9 @@ class ListFormatParser(object):
# priority (or placeholder space) # priority (or placeholder space)
'P': lambda t: t.priority() if t.priority() else ' ', 'P': lambda t: t.priority() if t.priority() else ' ',
# raw text
'r': lambda t: t.source(),
# text # text
's': lambda t: t.text(), 's': lambda t: t.text(),
......
...@@ -27,7 +27,7 @@ class MultiCommand(ExpressionCommand): ...@@ -27,7 +27,7 @@ class MultiCommand(ExpressionCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(MultiCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.todos = [] self.todos = []
...@@ -114,7 +114,7 @@ class MultiCommand(ExpressionCommand): ...@@ -114,7 +114,7 @@ class MultiCommand(ExpressionCommand):
raise NotImplementedError raise NotImplementedError
def execute(self): def execute(self):
if not super(MultiCommand, self).execute(): if not super().execute():
return False return False
self._process_flags() self._process_flags()
......
...@@ -50,7 +50,7 @@ class PrettyPrinter(Printer): ...@@ -50,7 +50,7 @@ class PrettyPrinter(Printer):
""" """
Constructor. Constructor.
""" """
super(PrettyPrinter, self).__init__() super().__init__()
self.filters = [] self.filters = []
def add_filter(self, p_filter): def add_filter(self, p_filter):
......
...@@ -29,4 +29,3 @@ class PrettyPrinterFilter(object): ...@@ -29,4 +29,3 @@ class PrettyPrinterFilter(object):
Applies a filter to p_todo_str and returns a modified version of it. Applies a filter to p_todo_str and returns a modified version of it.
""" """
raise NotImplementedError raise NotImplementedError
...@@ -37,9 +37,26 @@ def _add_months(p_sourcedate, p_months): ...@@ -37,9 +37,26 @@ def _add_months(p_sourcedate, p_months):
return date(year, month, day) return date(year, month, day)
def _add_business_days(p_sourcedate, p_bdays):
""" Adds a number of business days to the source date. """
result = p_sourcedate
delta = 1 if p_bdays > 0 else -1
while abs(p_bdays) > 0:
result += timedelta(delta)
weekday = result.weekday()
if weekday >= 5:
continue
p_bdays = p_bdays - 1 if delta > 0 else p_bdays + 1
return result
def _convert_pattern(p_length, p_periodunit, p_offset=None): def _convert_pattern(p_length, p_periodunit, p_offset=None):
""" """
Converts a pattern in the form [0-9][dwmy] and returns a date from the Converts a pattern in the form [0-9][dwmyb] and returns a date from the
offset with the period of time added to it. offset with the period of time added to it.
""" """
result = None result = None
...@@ -55,6 +72,8 @@ def _convert_pattern(p_length, p_periodunit, p_offset=None): ...@@ -55,6 +72,8 @@ def _convert_pattern(p_length, p_periodunit, p_offset=None):
result = _add_months(p_offset, p_length) result = _add_months(p_offset, p_length)
elif p_periodunit == 'y': elif p_periodunit == 'y':
result = _add_months(p_offset, p_length * 12) result = _add_months(p_offset, p_length * 12)
elif p_periodunit == 'b':
result = _add_business_days(p_offset, p_length)
return result return result
...@@ -98,7 +117,8 @@ def relative_date_to_date(p_date, p_offset=None): ...@@ -98,7 +117,8 @@ def relative_date_to_date(p_date, p_offset=None):
p_date = p_date.lower() p_date = p_date.lower()
p_offset = p_offset or date.today() p_offset = p_offset or date.today()
relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmy])$', p_date, re.I) relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmyb])$',
p_date, re.I)
monday = 'mo(n(day)?)?$' monday = 'mo(n(day)?)?$'
tuesday = 'tu(e(sday)?)?$' tuesday = 'tu(e(sday)?)?$'
......
...@@ -48,17 +48,14 @@ class TodoBase(object): ...@@ -48,17 +48,14 @@ class TodoBase(object):
Returns a tag value associated with p_key. Returns p_default if p_key Returns a tag value associated with p_key. Returns p_default if p_key
does not exist (which defaults to None). does not exist (which defaults to None).
""" """
values = self.tag_values(p_key) return self.tag_values(p_key)[0] if p_key in self.fields['tags'] else p_default
return values[0] if len(values) else p_default
def tag_values(self, p_key): def tag_values(self, p_key):
""" """
Returns a list of all tag values associated with p_key. Returns Returns a list of all tag values associated with p_key. Returns
empty list if p_key does not exist. empty list if p_key does not exist.
""" """
tags = self.fields['tags'] return self.fields['tags'][p_key] if p_key in self.fields['tags'] else []
matches = [tag[1] for tag in tags if tag[0] == p_key]
return matches if len(matches) else []
def has_tag(self, p_key, p_value=""): def has_tag(self, p_key, p_value=""):
""" """
...@@ -66,14 +63,28 @@ class TodoBase(object): ...@@ -66,14 +63,28 @@ class TodoBase(object):
value is passed, it will only return true when there exists a tag with value is passed, it will only return true when there exists a tag with
the given key-value combination. the given key-value combination.
""" """
result = [t for t in self.tag_values(p_key) tags = self.fields['tags']
if p_value == "" or t == p_value] return p_key in tags and (p_value == "" or p_value in tags[p_key])
return len(result) > 0
def add_tag(self, p_key, p_value): def add_tag(self, p_key, p_value):
""" Adds a tag to the todo. """ """ Adds a tag to the todo. """
self.set_tag(p_key, p_value, True) self.set_tag(p_key, p_value, True)
def _remove_tag_helper(self, p_key, p_value):
"""
Removes a tag from the internal todo dictionary. Only those instances
with the given value are removed. If the value is empty, all tags with
the given key are removed.
"""
tags = self.fields['tags']
try:
tags[p_key] = [t for t in tags[p_key] if p_value != "" and t != p_value]
if len(tags[p_key]) == 0:
del tags[p_key]
except KeyError:
pass
def set_tag(self, p_key, p_value="", p_force_add=False, p_old_value=""): def set_tag(self, p_key, p_value="", p_force_add=False, p_old_value=""):
""" """
Sets a occurrence of the tag identified by p_key. Sets an arbitrary Sets a occurrence of the tag identified by p_key. Sets an arbitrary
...@@ -92,12 +103,11 @@ class TodoBase(object): ...@@ -92,12 +103,11 @@ class TodoBase(object):
self.remove_tag(p_key, p_old_value) self.remove_tag(p_key, p_old_value)
return return
tags = self.fields['tags']
value = p_old_value if p_old_value else self.tag_value(p_key) value = p_old_value if p_old_value else self.tag_value(p_key)
if not p_force_add and value: if not p_force_add and value:
# remove old value from the tags self._remove_tag_helper(p_key, value)
self.fields['tags'] = [t for t in self.fields['tags']
if not (t[0] == p_key and t[1] == value)]
self.src = re.sub( self.src = re.sub(
r'\b' + p_key + ':' + value + r'\b', r'\b' + p_key + ':' + value + r'\b',
...@@ -107,7 +117,10 @@ class TodoBase(object): ...@@ -107,7 +117,10 @@ class TodoBase(object):
else: else:
self.src += ' ' + p_key + ':' + p_value self.src += ' ' + p_key + ':' + p_value
self.fields['tags'].append((p_key, p_value)) try:
tags[p_key].append(p_value)
except KeyError:
tags[p_key] = [p_value]
def remove_tag(self, p_key, p_value=""): def remove_tag(self, p_key, p_value=""):
""" """
...@@ -116,12 +129,7 @@ class TodoBase(object): ...@@ -116,12 +129,7 @@ class TodoBase(object):
removed. removed.
Else, only those tags with the value will be removed. Else, only those tags with the value will be removed.
""" """
self._remove_tag_helper(p_key, p_value)
# Build a new list that excludes the specified tag, match by value when
# p_value is given.
self.fields['tags'] = [t for t in self.fields['tags']
if not (t[0] == p_key and (p_value == "" or
t[1] == p_value))]
# when value == "", match any value having key p_key # when value == "", match any value having key p_key
value = p_value if p_value != "" else r'\S+' value = p_value if p_value != "" else r'\S+'
...@@ -132,7 +140,8 @@ class TodoBase(object): ...@@ -132,7 +140,8 @@ class TodoBase(object):
Returns a list of tuples with key-value pairs representing tags in Returns a list of tuples with key-value pairs representing tags in
this todo item. this todo item.
""" """
return self.fields['tags'] tags = self.fields['tags']
return [(t, v) for t in tags for v in tags[t]]
def set_priority(self, p_priority): def set_priority(self, p_priority):
""" """
......
...@@ -18,11 +18,35 @@ ...@@ -18,11 +18,35 @@
A list of todo items. A list of todo items.
""" """
import types
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Graph import DirectedGraph
from topydo.lib.TodoListBase import TodoListBase from topydo.lib.TodoListBase import TodoListBase
def _needs_dependencies(p_function):
"""
A decorator that triggers the population of the dependency tree in a
TodoList (and other administration). The decorator should be applied to
methods of TodoList that require dependency information.
"""
def build_dependency_information(p_todolist):
for todo in p_todolist._todos:
p_todolist._register_todo(todo)
def inner(self, *args, **kwargs):
if not self._initialized:
self._initialized = True
from topydo.lib.Graph import DirectedGraph
self._depgraph = DirectedGraph()
build_dependency_information(self)
return p_function(self, *args, **kwargs)
return inner
class TodoList(TodoListBase): class TodoList(TodoListBase):
""" """
Provides operations for a todo list, such as adding items, removing them, Provides operations for a todo list, such as adding items, removing them,
...@@ -37,21 +61,27 @@ class TodoList(TodoListBase): ...@@ -37,21 +61,27 @@ class TodoList(TodoListBase):
Should be given a list of strings, each element a single todo string. Should be given a list of strings, each element a single todo string.
The string will be parsed. The string will be parsed.
""" """
self._initialized = False # whether dependency information was
# initialized
# initialize these first because the constructor calls add_list # initialize these first because the constructor calls add_list
self._tododict = {} # hash(todo) to todo lookup self._tododict = {} # hash(todo) to todo lookup
self._depgraph = DirectedGraph() self._parentdict = {} # dependency id => parent todo
self._depgraph = None
super(TodoList, self).__init__(p_todostrings) super().__init__(p_todostrings)
@_needs_dependencies
def todo_by_dep_id(self, p_dep_id): def todo_by_dep_id(self, p_dep_id):
""" """
Returns the todo that has the id tag set to the value p_dep_id. Returns the todo that has the id tag set to the value p_dep_id.
There is only one such task, the behavior is undefined when a tag has There is only one such task, the behavior is undefined when a todo item
more than one id tag. has more than one id tag.
""" """
hits = [t for t in self._todos if t.tag_value('id') == p_dep_id] try:
return self._parentdict[p_dep_id]
return hits[0] if len(hits) else None except KeyError:
return None
def _maintain_dep_graph(self, p_todo): def _maintain_dep_graph(self, p_todo):
""" """
...@@ -61,6 +91,7 @@ class TodoList(TodoListBase): ...@@ -61,6 +91,7 @@ class TodoList(TodoListBase):
dep_id = p_todo.tag_value('id') dep_id = p_todo.tag_value('id')
# maintain dependency graph # maintain dependency graph
if dep_id: if dep_id:
self._parentdict[dep_id] = p_todo
self._depgraph.add_node(hash(p_todo)) self._depgraph.add_node(hash(p_todo))
# connect all tasks we have in memory so far that refer to this # connect all tasks we have in memory so far that refer to this
...@@ -70,31 +101,41 @@ class TodoList(TodoListBase): ...@@ -70,31 +101,41 @@ class TodoList(TodoListBase):
self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id) self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id)
for child in p_todo.tag_values('p'): for dep_id in p_todo.tag_values('p'):
parent = self.todo_by_dep_id(child) try:
if parent: parent = self._parentdict[dep_id]
self._depgraph.add_edge(hash(parent), hash(p_todo), child) self._depgraph.add_edge(hash(parent), hash(p_todo), dep_id)
except KeyError:
pass
def _register_todo(self, p_todo):
self._maintain_dep_graph(p_todo)
self._tododict[hash(p_todo)] = p_todo
def add_todos(self, p_todos): def add_todos(self, p_todos):
for todo in p_todos: super().add_todos(p_todos)
self._todos.append(todo)
self._tododict[hash(todo)] = todo
self._maintain_dep_graph(todo)
self._update_todo_ids() for todo in self._todos:
self._update_parent_cache() todo.parents = types.MethodType(lambda i: self.parents(i), todo)
self.dirty = True
# only do administration when the dependency info is initialized,
# otherwise we postpone it until it's really needed (through the
# _needs_dependencies decorator)
if self._initialized:
self._register_todo(todo)
def delete(self, p_todo): def delete(self, p_todo):
""" Deletes a todo item from the list. """ """ Deletes a todo item from the list. """
try: try:
number = self._todos.index(p_todo) number = self._todos.index(p_todo)
for child in self.children(p_todo): if p_todo.has_tag('id'):
self.remove_dependency(p_todo, child) for child in self.children(p_todo):
self.remove_dependency(p_todo, child)
for parent in self.parents(p_todo): if p_todo.has_tag('p'):
self.remove_dependency(parent, p_todo) for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo)
del self._todos[number] del self._todos[number]
self._update_todo_ids() self._update_todo_ids()
...@@ -104,6 +145,7 @@ class TodoList(TodoListBase): ...@@ -104,6 +145,7 @@ class TodoList(TodoListBase):
# todo item couldn't be found, ignore # todo item couldn't be found, ignore
pass pass
@_needs_dependencies
def add_dependency(self, p_from_todo, p_to_todo): def add_dependency(self, p_from_todo, p_to_todo):
""" Adds a dependency from task 1 to task 2. """ """ Adds a dependency from task 1 to task 2. """
def find_next_id(): def find_next_id():
...@@ -117,7 +159,8 @@ class TodoList(TodoListBase): ...@@ -117,7 +159,8 @@ class TodoList(TodoListBase):
Returns True if there exists a todo with the given parent ID. Returns True if there exists a todo with the given parent ID.
""" """
for todo in self._todos: for todo in self._todos:
if todo.has_tag('id', str(p_id)): number = str(p_id)
if todo.has_tag('id', number) or todo.has_tag('p', number):
return True return True
return False return False
...@@ -158,11 +201,11 @@ class TodoList(TodoListBase): ...@@ -158,11 +201,11 @@ class TodoList(TodoListBase):
p_to_todo.add_tag('p', dep_id) p_to_todo.add_tag('p', dep_id)
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id) self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id)
self._update_parent_cache()
append_projects_to_subtodo() append_projects_to_subtodo()
append_contexts_to_subtodo() append_contexts_to_subtodo()
self.dirty = True self.dirty = True
@_needs_dependencies
def remove_dependency(self, p_from_todo, p_to_todo): def remove_dependency(self, p_from_todo, p_to_todo):
""" Removes a dependency between two todos. """ """ Removes a dependency between two todos. """
dep_id = p_from_todo.tag_value('id') dep_id = p_from_todo.tag_value('id')
...@@ -170,13 +213,14 @@ class TodoList(TodoListBase): ...@@ -170,13 +213,14 @@ class TodoList(TodoListBase):
if dep_id: if dep_id:
p_to_todo.remove_tag('p', dep_id) p_to_todo.remove_tag('p', dep_id)
self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo)) self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo))
self._update_parent_cache()
if not self.children(p_from_todo, True): if not self.children(p_from_todo, True):
p_from_todo.remove_tag('id') p_from_todo.remove_tag('id')
del self._parentdict[dep_id]
self.dirty = True self.dirty = True
@_needs_dependencies
def parents(self, p_todo, p_only_direct=False): def parents(self, p_todo, p_only_direct=False):
""" """
Returns a list of parent todos that (in)directly depend on the Returns a list of parent todos that (in)directly depend on the
...@@ -186,6 +230,7 @@ class TodoList(TodoListBase): ...@@ -186,6 +230,7 @@ class TodoList(TodoListBase):
hash(p_todo), not p_only_direct) hash(p_todo), not p_only_direct)
return [self._tododict[parent] for parent in parents] return [self._tododict[parent] for parent in parents]
@_needs_dependencies
def children(self, p_todo, p_only_direct=False): def children(self, p_todo, p_only_direct=False):
""" """
Returns a list of child todos that the given todo (in)directly depends Returns a list of child todos that the given todo (in)directly depends
...@@ -195,6 +240,7 @@ class TodoList(TodoListBase): ...@@ -195,6 +240,7 @@ class TodoList(TodoListBase):
self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct) self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct)
return [self._tododict[child] for child in children] return [self._tododict[child] for child in children]
@_needs_dependencies
def clean_dependencies(self): def clean_dependencies(self):
""" """
Cleans the dependency graph. Cleans the dependency graph.
...@@ -219,6 +265,7 @@ class TodoList(TodoListBase): ...@@ -219,6 +265,7 @@ class TodoList(TodoListBase):
value = todo.tag_value('id') value = todo.tag_value('id')
if not self._depgraph.has_edge_id(value): if not self._depgraph.has_edge_id(value):
remove_tag(todo, 'id', value) remove_tag(todo, 'id', value)
del self._parentdict[value]
def clean_orphan_relations(): def clean_orphan_relations():
""" """
...@@ -237,12 +284,3 @@ class TodoList(TodoListBase): ...@@ -237,12 +284,3 @@ class TodoList(TodoListBase):
clean_parent_relations() clean_parent_relations()
clean_orphan_relations() clean_orphan_relations()
def _update_parent_cache(self):
"""
Sets the attribute to the list of parents, such that others may access
it outside this todo list.
This is used for calculating the average importance, that requires
access to a todo's parents.
"""
for todo in self._todos:
todo.attributes['parents'] = self.parents(todo)
...@@ -54,6 +54,12 @@ class TodoListBase(object): ...@@ -54,6 +54,12 @@ class TodoListBase(object):
self.add_list(p_todostrings) self.add_list(p_todostrings)
self._dirty = False self._dirty = False
def __iter__(self):
"""
Allows use of `for my_todo in todolist` constructs.
"""
return iter(self._todos)
def todo(self, p_identifier): def todo(self, p_identifier):
""" """
The _todos list has the same order as in the backend store (usually The _todos list has the same order as in the backend store (usually
...@@ -95,11 +101,11 @@ class TodoListBase(object): ...@@ -95,11 +101,11 @@ class TodoListBase(object):
# the expression is a string and no leading zeroes, # the expression is a string and no leading zeroes,
# treat it as an integer # treat it as an integer
raise TypeError raise TypeError
except TypeError: except TypeError as te:
try: try:
result = self._todos[int(p_identifier) - 1] result = self._todos[int(p_identifier) - 1]
except IndexError: except (ValueError, IndexError):
raise InvalidTodoException raise InvalidTodoException from te
return result return result
...@@ -251,8 +257,8 @@ class TodoListBase(object): ...@@ -251,8 +257,8 @@ class TodoListBase(object):
return self._todo_id_map[p_todo] return self._todo_id_map[p_todo]
else: else:
return self._todos.index(p_todo) + 1 return self._todos.index(p_todo) + 1
except (ValueError, KeyError): except (ValueError, KeyError) as ex:
raise InvalidTodoException raise InvalidTodoException from ex
def _update_todo_ids(self): def _update_todo_ids(self):
# the idea is to have a hash that is independent of the position of the # the idea is to have a hash that is independent of the position of the
......
...@@ -33,7 +33,7 @@ _NORMAL_HEAD_MATCH = re.compile( ...@@ -33,7 +33,7 @@ _NORMAL_HEAD_MATCH = re.compile(
r'(\((?P<priority>[A-Z])\) )?' + '((?P<creationDate>' + _DATE_MATCH + r'(\((?P<priority>[A-Z])\) )?' + '((?P<creationDate>' + _DATE_MATCH +
') )?(?P<rest>.*)') ') )?(?P<rest>.*)')
_TAG_MATCH = re.compile('(?P<key>[^:]+):(?P<value>.+)') _TAG_MATCH = re.compile('(?P<tag>[^:]+):(?P<value>.+)')
_PROJECT_MATCH = re.compile(r'\+(\S*\w)') _PROJECT_MATCH = re.compile(r'\+(\S*\w)')
_CONTEXT_MATCH = re.compile(r'@(\S*\w)') _CONTEXT_MATCH = re.compile(r'@(\S*\w)')
...@@ -57,7 +57,7 @@ def parse_line(p_string): ...@@ -57,7 +57,7 @@ def parse_line(p_string):
'text': "", 'text': "",
'projects': [], 'projects': [],
'contexts': [], 'contexts': [],
'tags': [] 'tags': {},
} }
completed_head = _COMPLETED_HEAD_MATCH.match(p_string) completed_head = _COMPLETED_HEAD_MATCH.match(p_string)
...@@ -94,10 +94,14 @@ def parse_line(p_string): ...@@ -94,10 +94,14 @@ def parse_line(p_string):
tag = _TAG_MATCH.match(word) tag = _TAG_MATCH.match(word)
if tag: if tag:
result['tags'].append((tag.group('key'), tag.group('value'))) tag_name = tag.group('tag')
continue tag_value = tag.group('value')
try:
result['text'] += word + ' ' result['tags'][tag_name].append(tag_value)
except KeyError:
result['tags'][tag_name] = [tag_value]
else:
result['text'] += word + ' '
# strip trailing space from resulting text # strip trailing space from resulting text
result['text'] = result['text'][:-1] result['text'] = result['text'][:-1]
......
...@@ -87,7 +87,7 @@ def get_terminal_size(p_getter=None): ...@@ -87,7 +87,7 @@ def get_terminal_size(p_getter=None):
occurs during running the unittest on Windows (but not on Linux?) occurs during running the unittest on Windows (but not on Linux?)
""" """
terminal_size = namedtuple('Terminal_Size', 'columns lines') terminal_size = namedtuple('Terminal_Size', 'columns lines')
sz = terminal_size((80, 24)) sz = terminal_size(80, 24)
return sz return sz
......
...@@ -25,7 +25,7 @@ from topydo.lib.ListFormat import ListFormatParser ...@@ -25,7 +25,7 @@ from topydo.lib.ListFormat import ListFormatParser
class PrettyPrinterFormatFilter(PrettyPrinterFilter): class PrettyPrinterFormatFilter(PrettyPrinterFilter):
def __init__(self, p_todolist, p_format=None): def __init__(self, p_todolist, p_format=None):
super(PrettyPrinterFormatFilter, self).__init__() super().__init__()
self.parser = ListFormatParser(p_todolist, p_format) self.parser = ListFormatParser(p_todolist, p_format)
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
......
...@@ -23,7 +23,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter): ...@@ -23,7 +23,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """ """ Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist): def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
......
...@@ -28,7 +28,7 @@ from topydo.ui.KeystateWidget import KeystateWidget ...@@ -28,7 +28,7 @@ from topydo.ui.KeystateWidget import KeystateWidget
from topydo.ui.TodoListWidget import TodoListWidget from topydo.ui.TodoListWidget import TodoListWidget
from topydo.ui.ViewWidget import ViewWidget from topydo.ui.ViewWidget import ViewWidget
from topydo.ui.ColumnLayout import columns from topydo.ui.ColumnLayout import columns
from topydo.lib.Config import config from topydo.lib.Config import config, ConfigError
from topydo.lib.Sorter import Sorter from topydo.lib.Sorter import Sorter
from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter
from topydo.lib.Utils import get_terminal_size from topydo.lib.Utils import get_terminal_size
...@@ -171,7 +171,12 @@ class UIApplication(CLIApplicationBase): ...@@ -171,7 +171,12 @@ class UIApplication(CLIApplicationBase):
""" """
p_output = p_output or self._output p_output = p_output or self._output
p_command = shlex.split(p_command) p_command = shlex.split(p_command)
(subcommand, args) = get_subcommand(p_command) try:
(subcommand, args) = get_subcommand(p_command)
except ConfigError as cerr:
self._print_to_console(
'Error: {}. Check your aliases configuration.'.format(cerr))
return
self._backup(subcommand, args) self._backup(subcommand, args)
......
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