Commit 25162351 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Add dependency support to the todo list.

parent 8d1f2e16
...@@ -4,6 +4,7 @@ A list of todo items. ...@@ -4,6 +4,7 @@ A list of todo items.
import re import re
import Graph
from PrettyPrinter import pretty_print from PrettyPrinter import pretty_print
import Todo import Todo
import View import View
...@@ -23,6 +24,8 @@ class TodoList(object): ...@@ -23,6 +24,8 @@ class TodoList(object):
The string will be parsed. The string will be parsed.
""" """
self._todos = [] self._todos = []
self._depgraph = Graph.DirectedGraph()
for string in p_todostrings: for string in p_todostrings:
self.add(string) self.add(string)
...@@ -40,9 +43,32 @@ class TodoList(object): ...@@ -40,9 +43,32 @@ class TodoList(object):
return result return result
def todo_by_dep_id(self, 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
more than one id tag.
"""
hits = [t for t in self._todos if t.tag_value('id') == p_dep_id]
return hits[0] if len(hits) else None
def add(self, p_src): def add(self, p_src):
""" """
Given a todo string, parse it and put it to the end of the list. Given a todo string, parse it and put it to the end of the list.
Also maintains the dependency graph to track the dependencies between
tasks.
The node ids are the todo numbers.
The edge ids are the numbers denoted by id: and p: tags.
For example:
(C) Parent task id:4
(B) Child task p:4
Then there will be an edge 1 --> 2 with ID 4.
""" """
if re.search(r'\S', p_src): if re.search(r'\S', p_src):
...@@ -50,12 +76,27 @@ class TodoList(object): ...@@ -50,12 +76,27 @@ class TodoList(object):
todo = Todo.Todo(p_src, number) todo = Todo.Todo(p_src, number)
self._todos.append(todo) self._todos.append(todo)
# maintain dependency graph
if todo.has_tag('id'):
self._depgraph.add_node(todo.number)
for child in todo.tag_values('p'):
parent = self.todo_by_dep_id(child)
if parent:
self._depgraph.add_edge(parent.number, todo.number, child)
def delete(self, p_number): def delete(self, p_number):
""" Deletes a todo item from the list. """ """ Deletes a todo item from the list. """
try: todo = self.todo(p_number)
if todo:
for child in self.children(p_number):
self.remove_dependency(todo.number, child.number)
for parent in self.parents(p_number):
self.remove_dependency(parent.number, todo.number)
del self._todos[p_number - 1] del self._todos[p_number - 1]
except IndexError:
pass # just ignore it
def count(self): def count(self):
""" Returns the number of todos on this list. """ """ Returns the number of todos on this list. """
...@@ -100,6 +141,64 @@ class TodoList(object): ...@@ -100,6 +141,64 @@ class TodoList(object):
""" """
return View.View(p_sorter, p_filters, self._todos) return View.View(p_sorter, p_filters, self._todos)
def add_dependency(self, p_number1, p_number2):
""" Adds a dependency from task 1 to task 2. """
def find_next_id():
"""
Find a new unused ID.
Unused means that no task has it as an 'id' value or as a 'p'
value.
"""
new_id = 1
while self._depgraph.has_edge_id(new_id):
new_id += 1
return '%d' % new_id
from_todo = self.todo(p_number1)
to_todo = self.todo(p_number2)
dep_id = None
if from_todo.has_tag('id'):
dep_id = from_todo.tag_value('id')
else:
dep_id = find_next_id()
to_todo.add_tag('p', dep_id)
self._depgraph.add_edge(p_number1, p_number2, int(dep_id))
def remove_dependency(self, p_number1, p_number2):
""" Removes a dependency between two todos. """
from_todo = self.todo(p_number1)
to_todo = self.todo(p_number2)
dep_id = from_todo.tag_value('id')
if dep_id:
to_todo.remove_tag('p', dep_id)
self._depgraph.remove_edge(p_number1, p_number2)
if not self.children(p_number1, True):
from_todo.remove_tag('id')
def parents(self, p_number, p_only_direct=False):
"""
Returns a list of parent todos that (in)directly depend on the
given todo.
"""
parents = self._depgraph.incoming_neighbors(p_number, not p_only_direct)
return [self.todo(parent) for parent in parents]
def children(self, p_number, p_only_direct=False):
"""
Returns a list of child todos that the given todo (in)directly depends
on.
"""
children = \
self._depgraph.outgoing_neighbors(p_number, not p_only_direct)
return [self.todo(child) for child in children]
def __str__(self): def __str__(self):
return '\n'.join(pretty_print(self._todos)) return '\n'.join(pretty_print(self._todos))
...@@ -115,3 +115,62 @@ class TodoListTester(unittest.TestCase): ...@@ -115,3 +115,62 @@ class TodoListTester(unittest.TestCase):
def test_count(self): def test_count(self):
""" Test that empty lines are not counted. """ """ Test that empty lines are not counted. """
self.assertEquals(self.todolist.count(), 5) self.assertEquals(self.todolist.count(), 5)
def test_todo_by_dep_id(self):
""" Tests that todos can be retrieved by their id tag. """
self.todolist.add("(C) Foo id:1")
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
class TodoListDependencyTester(unittest.TestCase):
def setUp(self):
self.todolist = TodoList.TodoList([])
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")
def test_check_dep(self):
children = self.todolist.children(1)
self.assertEqual([todo.source() for todo in children], \
['Bar p:1', 'Baz p:1 id:2', 'Buzz p:2'])
children = self.todolist.children(1, True)
self.assertEqual([todo.source() for todo in children], \
['Bar p:1', 'Baz p:1 id:2'])
children = self.todolist.children(3)
self.assertEqual([todo.source() for todo in children], \
['Buzz p:2'])
parents = self.todolist.parents(4)
self.assertEqual([todo.source() for todo in parents], \
['Foo id:1', 'Baz p:1 id:2'])
parents = self.todolist.parents(4, True)
self.assertEqual([todo.source() for todo in parents], \
['Baz p:1 id:2'])
self.assertEqual(self.todolist.children(2), [])
self.assertEqual(self.todolist.parents(1), [])
def test_remove_dep1(self):
self.todolist.remove_dependency(3, 4)
self.assertFalse(self.todolist.todo(3).has_tag('id'))
self.assertFalse(self.todolist.todo(4).has_tag('p'))
def test_remove_dep2(self):
old = str(self.todolist)
self.todolist.remove_dependency(1, 4)
self.assertEquals(str(self.todolist),old)
def test_remove_task(self):
self.todolist.delete(3)
self.assertFalse(self.todolist.todo(3).has_tag('p', '2'))
children = self.todolist.children(1)
self.assertEqual([todo.source() for todo in children], \
['Bar p:1'])
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