Commit 61253864 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Load dependency information on demand

Not all commands always need dependencies (pri, depri, ls -x), but all
administration is still built up every time a TodoList is instantiated.
Now, as soon as something dependency related is requested from the
TodoList, the dependency graph and caches are initialized.

A decorator is used to mark methods in TodoList that require an
initialized graph.

This change impacts the calculation of the average importance (based on
the todo's ancestors). Importance doesn't have a notion of TodoList (and
I would like to keep it that way), so the calculation needs access to
the parents somehow. This was done with a parents attribute stored with
each Todo item, carefully kept up to date by the TodoList. This is
cumbersome, instead, now a Todo item gets a parents() method that
obtains the parents from the TodoList that keeps the todo item.
parent 2365e99d
...@@ -388,6 +388,19 @@ class TodoListDependencyTester(TopydoTest): ...@@ -388,6 +388,19 @@ 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):
""" """
......
...@@ -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)
...@@ -18,11 +18,32 @@ ...@@ -18,11 +18,32 @@
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.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
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,6 +58,9 @@ class TodoList(TodoListBase): ...@@ -37,6 +58,9 @@ 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._parentdict = {} # dependency id => parent todo self._parentdict = {} # dependency id => parent todo
...@@ -44,6 +68,7 @@ class TodoList(TodoListBase): ...@@ -44,6 +68,7 @@ class TodoList(TodoListBase):
super().__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.
...@@ -74,30 +99,39 @@ class TodoList(TodoListBase): ...@@ -74,30 +99,39 @@ 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 dep_id in p_todo.tag_values('p'): for dep_id in p_todo.tag_values('p'):
parent = self.todo_by_dep_id(dep_id) try:
if parent: parent = self._parentdict[dep_id]
self._depgraph.add_edge(hash(parent), hash(p_todo), dep_id) 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 for todo in self._todos:
self._maintain_dep_graph(todo) todo.parents = types.MethodType(lambda i: self.parents(i), todo)
self._update_todo_ids() # only do administration when the dependency info is initialized,
self._update_parent_cache() # otherwise we postpone it until it's really needed (through the
self.dirty = True # _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('p') or 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): for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo) self.remove_dependency(parent, p_todo)
del self._todos[number] del self._todos[number]
self._update_todo_ids() self._update_todo_ids()
...@@ -107,6 +141,7 @@ class TodoList(TodoListBase): ...@@ -107,6 +141,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():
...@@ -162,11 +197,11 @@ class TodoList(TodoListBase): ...@@ -162,11 +197,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')
...@@ -174,7 +209,6 @@ class TodoList(TodoListBase): ...@@ -174,7 +209,6 @@ 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')
...@@ -182,6 +216,7 @@ class TodoList(TodoListBase): ...@@ -182,6 +216,7 @@ class TodoList(TodoListBase):
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
...@@ -191,6 +226,7 @@ class TodoList(TodoListBase): ...@@ -191,6 +226,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
...@@ -200,6 +236,7 @@ class TodoList(TodoListBase): ...@@ -200,6 +236,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.
...@@ -243,12 +280,3 @@ class TodoList(TodoListBase): ...@@ -243,12 +280,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)
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