def __init__(self, p_todostrings): """ Should be given a list of strings, each element a single todo string. The string will be parsed. """ # initialize these first because the constructor calls add_list self._tododict = {} # hash(todo) to todo lookup self._depgraph = DirectedGraph() super(TodoList, self).__init__(p_todostrings)
def setUp(self): super().setUp() self.graph = DirectedGraph() self.graph.add_edge(1, 2, 1) self.graph.add_edge(2, 4, "Test") self.graph.add_edge(4, 3) self.graph.add_edge(4, 6) self.graph.add_edge(6, 2) self.graph.add_edge(1, 3) self.graph.add_edge(3, 5)
def __init__(self, p_todostrings): """ Should be given a list of strings, each element a single todo string. The string will be parsed. """ self._todos = [] self._tododict = {} # hash(todo) to todo lookup self._depgraph = DirectedGraph() self._todo_id_map = {} self._id_todo_map = {} self.add_list(p_todostrings) self.dirty = False
class TodoList(TodoListBase): """ Provides operations for a todo list, such as adding items, removing them, etc. The list is usually a complete list found in the program's input (e.g. a todo.txt file), not an arbitrary set of todo items. """ def __init__(self, p_todostrings): """ Should be given a list of strings, each element a single todo string. The string will be parsed. """ # initialize these first because the constructor calls add_list self._tododict = {} # hash(todo) to todo lookup self._depgraph = DirectedGraph() super(TodoList, self).__init__(p_todostrings) 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 _maintain_dep_graph(self, p_todo): """ Makes sure that the dependency graph is consistent according to the given todo. """ dep_id = p_todo.tag_value('id') # maintain dependency graph if dep_id: self._depgraph.add_node(hash(p_todo)) # connect all tasks we have in memory so far that refer to this # task for dep in \ [dep for dep in self._todos if dep.has_tag('p', dep_id)]: self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id) for child in p_todo.tag_values('p'): parent = self.todo_by_dep_id(child) if parent: self._depgraph.add_edge(hash(parent), hash(p_todo), child) def add_todos(self, p_todos): for todo in p_todos: self._todos.append(todo) self._tododict[hash(todo)] = todo self._maintain_dep_graph(todo) self._update_todo_ids() self._update_parent_cache() self.dirty = True def delete(self, p_todo): """ Deletes a todo item from the list. """ try: number = self._todos.index(p_todo) for child in self.children(p_todo): self.remove_dependency(p_todo, child) for parent in self.parents(p_todo): self.remove_dependency(parent, p_todo) del self._todos[number] self._update_todo_ids() self.dirty = True except ValueError: # todo item couldn't be found, ignore pass def add_dependency(self, p_from_todo, p_to_todo): """ 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. """ def id_exists(p_id): """ Returns True if there exists a todo with the given parent ID. """ for todo in self._todos: if todo.has_tag('id', str(p_id)): return True return False new_id = 1 while id_exists(new_id): new_id += 1 return str(new_id) def append_projects_to_subtodo(): """ Appends projects in the parent todo item that are not present in the sub todo item. """ if config().append_parent_projects(): for project in p_from_todo.projects() - p_to_todo.projects(): self.append(p_to_todo, "+{}".format(project)) def append_contexts_to_subtodo(): """ Appends contexts in the parent todo item that are not present in the sub todo item. """ if config().append_parent_contexts(): for context in p_from_todo.contexts() - p_to_todo.contexts(): self.append(p_to_todo, "@{}".format(context)) if p_from_todo != p_to_todo and not self._depgraph.has_edge( hash(p_from_todo), hash(p_to_todo)): dep_id = None if p_from_todo.has_tag('id'): dep_id = p_from_todo.tag_value('id') else: dep_id = find_next_id() p_from_todo.set_tag('id', 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._update_parent_cache() append_projects_to_subtodo() append_contexts_to_subtodo() self.dirty = True def remove_dependency(self, p_from_todo, p_to_todo): """ Removes a dependency between two todos. """ dep_id = p_from_todo.tag_value('id') if dep_id: p_to_todo.remove_tag('p', dep_id) self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo)) self._update_parent_cache() if not self.children(p_from_todo, True): p_from_todo.remove_tag('id') self.dirty = True def parents(self, p_todo, p_only_direct=False): """ Returns a list of parent todos that (in)directly depend on the given todo. """ parents = self._depgraph.incoming_neighbors( hash(p_todo), not p_only_direct) return [self._tododict[parent] for parent in parents] def children(self, p_todo, p_only_direct=False): """ Returns a list of child todos that the given todo (in)directly depends on. """ children = \ self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct) return [self._tododict[child] for child in children] def clean_dependencies(self): """ Cleans the dependency graph. This is achieved by performing a transitive reduction on the dependency graph and removing unused dependency ids from the graph (in that order). """ def clean_by_tag(tag_name): """ Generic function to handle 'p' and 'id' tags. """ for todo in [todo for todo in self._todos if todo.has_tag(tag_name)]: value = todo.tag_value(tag_name) if not self._depgraph.has_edge_id(value): todo.remove_tag(tag_name, value) self.dirty = True self._depgraph.transitively_reduce() clean_by_tag('p') clean_by_tag('id') 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)
class GraphTest(TopydoTest): def setUp(self): super().setUp() self.graph = DirectedGraph() self.graph.add_edge(1, 2, 1) self.graph.add_edge(2, 4, "Test") self.graph.add_edge(4, 3) self.graph.add_edge(4, 6) self.graph.add_edge(6, 2) self.graph.add_edge(1, 3) self.graph.add_edge(3, 5) # 1 # / \ # v v # />2 />3 # / | / | # / v / v # 6 <- 4 5 def test_has_nodes(self): for i in range(1, 7): self.assertTrue(self.graph.has_node(i)) def test_has_edge_ids(self): self.assertTrue(self.graph.has_edge_id(1)) self.assertTrue(self.graph.has_edge_id("Test")) self.assertFalse(self.graph.has_edge_id("1")) def test_incoming_neighbors1(self): self.assertEqual(self.graph.incoming_neighbors(1), set()) def test_edge_id_of_nonexistent_edge(self): self.assertFalse(self.graph.edge_id(1, 6)) def test_incoming_neighbors2(self): self.assertEqual(self.graph.incoming_neighbors(2), set([1, 6])) def test_incoming_neighbors3(self): self.assertEqual(self.graph.incoming_neighbors(1, True), set()) def test_incoming_neighbors4(self): self.assertEqual(self.graph.incoming_neighbors(5, True), set([1, 2, 3, 4, 6])) def test_outgoing_neighbors1(self): self.assertEqual(self.graph.outgoing_neighbors(1), set([2, 3])) def test_outgoing_neighbors2(self): self.assertEqual(self.graph.outgoing_neighbors(2), set([4])) def test_outgoing_neighbors3(self): self.assertEqual(self.graph.outgoing_neighbors(1, True), set([2, 3, 4, 5, 6])) def test_outgoing_neighbors4(self): self.assertEqual(self.graph.outgoing_neighbors(3), set([5])) def test_outgoing_neighbors5(self): self.assertEqual(self.graph.outgoing_neighbors(5), set([])) def test_remove_edge1(self): self.graph.remove_edge(1, 2) self.assertFalse(self.graph.has_path(1, 4)) self.assertTrue(self.graph.has_path(2, 4)) self.assertFalse(self.graph.has_edge_id(1)) def test_remove_edge2(self): self.graph.remove_edge(3, 5, True) self.assertFalse(self.graph.has_path(1, 5)) self.assertFalse(self.graph.has_node(5)) def test_remove_edge3(self): self.graph.remove_edge(3, 5, False) self.assertFalse(self.graph.has_path(1, 5)) self.assertTrue(self.graph.has_node(5)) def test_remove_edge4(self): """ Remove non-existing edge. """ self.graph.remove_edge(4, 5) def test_remove_edge5(self): self.graph.remove_edge(3, 5, True) self.assertFalse(self.graph.has_path(1, 5)) self.assertFalse(self.graph.has_node(5)) def test_remove_edge6(self): self.graph.remove_edge(1, 3, True) self.assertTrue(self.graph.has_path(1, 5)) def test_remove_node1(self): self.graph.remove_node(2) self.assertTrue(self.graph.has_node(1)) self.assertTrue(self.graph.has_node(4)) self.assertTrue(self.graph.has_node(6)) self.assertFalse(self.graph.has_node(2)) self.assertFalse(self.graph.has_edge(2, 4)) self.assertFalse(self.graph.has_edge(1, 2)) def test_remove_node2(self): self.graph.remove_node(3, True) self.assertFalse(self.graph.has_node(5)) self.assertFalse(self.graph.has_edge(1, 3)) self.assertFalse(self.graph.has_edge(3, 5)) self.assertFalse(self.graph.has_path(1, 5)) def test_remove_node3(self): self.graph.remove_node(3, False) self.assertTrue(self.graph.has_node(5)) self.assertFalse(self.graph.has_edge(1, 3)) self.assertFalse(self.graph.has_edge(3, 5)) self.assertFalse(self.graph.has_path(1, 5)) def test_transitive_reduce1(self): self.graph.transitively_reduce() self.assertTrue(self.graph.has_edge(4, 3)) self.assertFalse(self.graph.has_edge(1, 3)) def test_add_double_edge(self): self.graph.add_edge(1, 3) self.graph.remove_edge(1, 3) # the one and only edge must be removed now self.assertFalse(self.graph.has_edge(1, 3)) def test_add_double_edge_with_id(self): self.graph.add_edge(1, 3, "Dummy") self.assertFalse(self.graph.has_edge_id("Dummy")) self.graph.remove_edge(1, 3) # the one and only edge must be removed now self.assertFalse(self.graph.has_edge(1, 3)) def test_str_output(self): out = 'digraph g {\n 1\n 1 -> 2 [label="1"]\n 1 -> 3\n 2\n 2 -> 4 [label="Test"]\n 3\n 3 -> 5\n 4\n 4 -> 3\n 4 -> 6\n 5\n 6\n 6 -> 2\n}\n' self.assertEqual(str(self.graph), out) def test_dot_output_without_labels(self): out = 'digraph g {\n 1\n 1 -> 2\n 1 -> 3\n 2\n 2 -> 4\n 3\n 3 -> 5\n 4\n 4 -> 3\n 4 -> 6\n 5\n 6\n 6 -> 2\n}\n' self.assertEqual(self.graph.dot(False), out)
class TodoList(TodoListBase): """ Provides operations for a todo list, such as adding items, removing them, etc. The list is usually a complete list found in the program's input (e.g. a todo.txt file), not an arbitrary set of todo items. """ def __init__(self, p_todostrings): """ Should be given a list of strings, each element a single todo string. The string will be parsed. """ self._todos = [] self._tododict = {} # hash(todo) to todo lookup self._depgraph = DirectedGraph() self._todo_id_map = {} self._id_todo_map = {} self.add_list(p_todostrings) self.dirty = False 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 _maintain_dep_graph(self, p_todo): """ Makes sure that the dependency graph is consistent according to the given todo. """ dep_id = p_todo.tag_value('id') # maintain dependency graph if dep_id: self._depgraph.add_node(hash(p_todo)) # connect all tasks we have in memory so far that refer to this # task for dep in \ [dep for dep in self._todos if dep.has_tag('p', dep_id)]: self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id) for child in p_todo.tag_values('p'): parent = self.todo_by_dep_id(child) if parent: self._depgraph.add_edge(hash(parent), hash(p_todo), child) def add_todos(self, p_todos): for todo in p_todos: self._todos.append(todo) self._tododict[hash(todo)] = todo self._maintain_dep_graph(todo) self._update_todo_ids() self._update_parent_cache() self.dirty = True def delete(self, p_todo): """ Deletes a todo item from the list. """ try: number = self._todos.index(p_todo) for child in self.children(p_todo): self.remove_dependency(p_todo, child) for parent in self.parents(p_todo): self.remove_dependency(parent, p_todo) del self._todos[number] self._update_todo_ids() self.dirty = True except ValueError: # todo item couldn't be found, ignore pass def add_dependency(self, p_from_todo, p_to_todo): """ 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. """ def id_exists(p_id): """ Returns True if there exists a todo with the given parent ID. """ for todo in self._todos: if todo.has_tag('id', str(p_id)): return True return False new_id = 1 while id_exists(new_id): new_id += 1 return str(new_id) def append_projects_to_subtodo(): """ Appends projects in the parent todo item that are not present in the sub todo item. """ if config().append_parent_projects(): for project in p_from_todo.projects() - p_to_todo.projects(): self.append(p_to_todo, "+{}".format(project)) if p_from_todo != p_to_todo and not self._depgraph.has_edge( hash(p_from_todo), hash(p_to_todo)): dep_id = None if p_from_todo.has_tag('id'): dep_id = p_from_todo.tag_value('id') else: dep_id = find_next_id() p_from_todo.set_tag('id', 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._update_parent_cache() append_projects_to_subtodo() self.dirty = True def remove_dependency(self, p_from_todo, p_to_todo): """ Removes a dependency between two todos. """ dep_id = p_from_todo.tag_value('id') if dep_id: p_to_todo.remove_tag('p', dep_id) self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo)) self._update_parent_cache() if not self.children(p_from_todo, True): p_from_todo.remove_tag('id') self.dirty = True def parents(self, p_todo, p_only_direct=False): """ Returns a list of parent todos that (in)directly depend on the given todo. """ parents = self._depgraph.incoming_neighbors(hash(p_todo), not p_only_direct) return [self._tododict[parent] for parent in parents] def children(self, p_todo, p_only_direct=False): """ Returns a list of child todos that the given todo (in)directly depends on. """ children = \ self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct) return [self._tododict[child] for child in children] def clean_dependencies(self): """ Cleans the dependency graph. This is achieved by performing a transitive reduction on the dependency graph and removing unused dependency ids from the graph (in that order). """ def clean_by_tag(tag_name): """ Generic function to handle 'p' and 'id' tags. """ for todo in [ todo for todo in self._todos if todo.has_tag(tag_name) ]: value = todo.tag_value(tag_name) if not self._depgraph.has_edge_id(value): todo.remove_tag(tag_name, value) self.dirty = True self._depgraph.transitively_reduce() clean_by_tag('p') clean_by_tag('id') 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)