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):
self.assertTrue(todolist.todo_by_dep_id('1'))
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):
"""
......
......@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
average = 0
parents = []
if 'parents' in p_todo.attributes:
try:
sum_importance = own_importance
parents = p_todo.attributes['parents']
parents = p_todo.parents()
for parent in parents:
sum_importance += importance(parent, p_ignore_weekend)
average = float(sum_importance) / float(1 + len(parents))
except AttributeError:
pass
return max(own_importance, average)
......@@ -18,11 +18,32 @@
A list of todo items.
"""
import types
from topydo.lib.Config import config
from topydo.lib.Graph import DirectedGraph
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):
"""
Provides operations for a todo list, such as adding items, removing them,
......@@ -37,6 +58,9 @@ class TodoList(TodoListBase):
Should be given a list of strings, each element a single todo string.
The string will be parsed.
"""
self._initialized = False # whether dependency information was
# initialized
# initialize these first because the constructor calls add_list
self._tododict = {} # hash(todo) to todo lookup
self._parentdict = {} # dependency id => parent todo
......@@ -44,6 +68,7 @@ class TodoList(TodoListBase):
super().__init__(p_todostrings)
@_needs_dependencies
def todo_by_dep_id(self, 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):
self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id)
for dep_id in p_todo.tag_values('p'):
parent = self.todo_by_dep_id(dep_id)
if parent:
try:
parent = self._parentdict[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):
for todo in p_todos:
self._todos.append(todo)
self._tododict[hash(todo)] = todo
self._maintain_dep_graph(todo)
super().add_todos(p_todos)
for todo in self._todos:
todo.parents = types.MethodType(lambda i: self.parents(i), todo)
self._update_todo_ids()
self._update_parent_cache()
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):
""" 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)
if p_todo.has_tag('p') or p_todo.has_tag('id'):
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)
for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo)
del self._todos[number]
self._update_todo_ids()
......@@ -107,6 +141,7 @@ class TodoList(TodoListBase):
# todo item couldn't be found, ignore
pass
@_needs_dependencies
def add_dependency(self, p_from_todo, p_to_todo):
""" Adds a dependency from task 1 to task 2. """
def find_next_id():
......@@ -162,11 +197,11 @@ class TodoList(TodoListBase):
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
@_needs_dependencies
def remove_dependency(self, p_from_todo, p_to_todo):
""" Removes a dependency between two todos. """
dep_id = p_from_todo.tag_value('id')
......@@ -174,7 +209,6 @@ class TodoList(TodoListBase):
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')
......@@ -182,6 +216,7 @@ class TodoList(TodoListBase):
self.dirty = True
@_needs_dependencies
def parents(self, p_todo, p_only_direct=False):
"""
Returns a list of parent todos that (in)directly depend on the
......@@ -191,6 +226,7 @@ class TodoList(TodoListBase):
hash(p_todo), not p_only_direct)
return [self._tododict[parent] for parent in parents]
@_needs_dependencies
def children(self, p_todo, p_only_direct=False):
"""
Returns a list of child todos that the given todo (in)directly depends
......@@ -200,6 +236,7 @@ class TodoList(TodoListBase):
self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct)
return [self._tododict[child] for child in children]
@_needs_dependencies
def clean_dependencies(self):
"""
Cleans the dependency graph.
......@@ -243,12 +280,3 @@ class TodoList(TodoListBase):
clean_parent_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