Repository: fniessink/next-action

Issue:
Child tasks inherit their parent's priority
max(parents' priorities) in case of multiple priorities

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0df435..3232b44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ### Added
 
+- Tasks are considered to have a priority that's the maximum of their own priority and the priorities of the task(s) they block, recursively. Closes #114.
 - Tasks are considered to have a due date that's the minimum of their own due date and the due dates of the task(s) they block, recursively. Closes #115.
 
 ## [1.3.0] - 2018-06-19
diff --git a/README.md b/README.md
index d259760..67e9fc7 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ $ next-action
 
 The next action is determined using priority. Due date is considered after priority, with tasks due earlier getting precedence over tasks due later. Creation date is considered after due date, with older tasks getting precedence over newer tasks. Finally, tasks that belong to more projects get precedence over tasks that belong to fewer projects.
 
-Several types of tasks are not considered when determining the next action:
+Several types of tasks can not be a next action:
 
 - completed tasks (~~`x This is a completed task`~~),
 - tasks with a creation date in the future (`9999-01-01 Start preparing for five-digit years`),
@@ -239,7 +239,7 @@ Buy groceries @store +DinnerParty before:meal
 Take out the garbage @home +DinnerParty due:2018-07-02
 ```
 
-Note how buying the groceries comes before taking out the garbage even though buying the groceries has no due date and taking out the garbage does. As buying groceries has to be done before cooking the meal and cooking the meal does have a due date, buying groceries takes on the same due date as cooking the meal.
+Note how buying the groceries comes before taking out the garbage even though buying the groceries has no due date and taking out the garbage does. As buying groceries has to be done before cooking the meal and cooking the meal does have a due date, buying groceries takes on the same due date as cooking the meal. Priority is taken into account in a similar way.
 
 Additional notes:
 
@@ -247,7 +247,9 @@ Additional notes:
 - Instead of `before` you can also use `p` (for "parent") because some other tools that work with *Todo.txt* files use that.
 - A task can block multiple other tasks by repeating the before key, e.g. `Buy groceries before:cooking and before:sending_invites`.
 - A task can be blocked by multiple other tasks by repeating the after key, e.g. `Eat meal after:cooking and after:setting_the_table`.
-- If a task blocks one or more tasks, the blocking task is considered to have a due date that's the minimum of its own due date and the due dates of the tasks it's blocking.
+- If a task blocks one or more tasks, the blocking task takes on the priority and due date of the tasks it is blocking:
+  - the blocking task is considered to have a priority that is the maximum of its own priority and the priorities of the tasks it is blocking, and
+  - the blocking task is considered to have a due date that is the minimum of its own due date and the due dates of the tasks it is blocking.
 
 ### Styling the output
 
@@ -427,9 +429,9 @@ To run the unit tests:
 
 ```console
 $ python -m unittest
-.................................................................................................................................................................................................................................
+.....................................................................................................................................................................................................................................
 ----------------------------------------------------------------------
-Ran 225 tests in 1.891s
+Ran 229 tests in 2.728s
 
 OK
 ```
@@ -440,9 +442,9 @@ To create the unit test coverage report run the unit tests under coverage with:
 
 ```console
 $ coverage run --branch -m unittest
-.................................................................................................................................................................................................................................
+.....................................................................................................................................................................................................................................
 ----------------------------------------------------------------------
-Ran 225 tests in 2.567s
+Ran 229 tests in 3.794s
 
 OK
 ```
@@ -454,7 +456,7 @@ $ coverage report --fail-under=100 --omit=".venv/*" --skip-covered
 Name    Stmts   Miss Branch BrPart  Cover
 -----------------------------------------
 -----------------------------------------
-TOTAL    1254      0    154      0   100%
+TOTAL    1282      0    160      0   100%
 
 25 files skipped due to complete coverage.
 ```
diff --git a/next_action/todotxt/task.py b/next_action/todotxt/task.py
index 1d238db..5f9d794 100644
--- a/next_action/todotxt/task.py
+++ b/next_action/todotxt/task.py
@@ -3,7 +3,7 @@
 import datetime
 import re
 import typing
-from typing import Optional, Sequence, Set
+from typing import List, Optional, Sequence, Set
 
 
 class Task(object):
@@ -30,7 +30,9 @@ class Task(object):
     def priority(self) -> Optional[str]:
         """ Return the priority of the task """
         match = re.match(r"\(([A-Z])\) ", self.text)
-        return match.group(1) if match else None
+        priorities: List[Optional[str]] = [match.group(1)] if match else []
+        priorities.extend([blocked_task.priority() for blocked_task in self.__blocked_tasks()])
+        return min(priorities, default=None, key=lambda priority: priority or "ZZZ")
 
     def priority_at_least(self, min_priority: Optional[str]) -> bool:
         """ Return whether the priority of task is at least the given priority. """
@@ -52,9 +54,9 @@ class Task(object):
 
     def due_date(self) -> Optional[datetime.date]:
         """ Return the due date of the task. """
-        due_dates = [self.__find_keyed_date("due") or datetime.date.max]
-        due_dates.extend([blocking.due_date() or datetime.date.max for blocking in self.__blocking()])
-        return None if min(due_dates) == datetime.date.max else min(due_dates)
+        due_dates = [self.__find_keyed_date("due")]
+        due_dates.extend([blocked_task.due_date() for blocked_task in self.__blocked_tasks()])
+        return min(due_dates, default=None, key=lambda due_date: due_date or datetime.date.max)
 
     def is_due(self, due_date: datetime.date) -> bool:
         """ Return whether the task is due on or before the given due date. """
@@ -104,7 +106,7 @@ class Task(object):
         match = re.search(r"\bid:(\S+)\b", self.text)
         return match.group(1) if match else ""
 
-    def __blocking(self) -> Sequence["Task"]:
+    def __blocked_tasks(self) -> Sequence["Task"]:
         """ Return the tasks this task is blocking. """
         return [task for task in self.tasks if not task.is_completed() and
                 (task.task_id() in self.parent_ids() or self.task_id() in task.child_ids())]


Test patch:
diff --git a/tests/unittests/test_pick_action.py b/tests/unittests/test_pick_action.py
index 5bb83a6..559e236 100644
--- a/tests/unittests/test_pick_action.py
+++ b/tests/unittests/test_pick_action.py
@@ -220,16 +220,6 @@ class DueTasks(PickActionTestCase):
         self.assertEqual([overdue, future_duedate],
                          pick_action.next_actions([no_duedate, future_duedate, overdue], self.namespace))
 
-    def test_due_date_of_blocked_task(self):
-        """ Test that a task that blocks a task with an earlier due date takes precendence. """
-        tasks = todotxt.Tasks()
-        due_first_but_blocked = todotxt.Task("Task id:1 due:2018-01-01", tasks=tasks)
-        blocking_task = todotxt.Task("Blocking before:1", tasks=tasks)
-        due_second = todotxt.Task("Task due:2018-02-01", tasks=tasks)
-        tasks.extend([due_first_but_blocked, blocking_task, due_second])
-        self.namespace.due = datetime.date.max
-        self.assertEqual([blocking_task, due_second], pick_action.next_actions(tasks, self.namespace))
-
 
 class MinimimPriorityTest(PickActionTest):
     """ Unit test for the mininum priority filter. """
@@ -282,3 +272,21 @@ class BlockedTasksTest(PickActionTest):
         child = todotxt.Task("Child 1 p:1 p:2", tasks=tasks)
         tasks.extend([parent1, parent2, child])
         self.assertEqual([child], pick_action.next_actions(tasks, self.namespace))
+
+    def test_due_date_of_blocked_task(self):
+        """ Test that a task that blocks a task with an earlier due date takes precendence. """
+        tasks = todotxt.Tasks()
+        due_first_but_blocked = todotxt.Task("Task id:1 due:2018-01-01", tasks=tasks)
+        blocking_task = todotxt.Task("Blocking before:1", tasks=tasks)
+        due_second = todotxt.Task("Task due:2018-02-01", tasks=tasks)
+        tasks.extend([due_first_but_blocked, blocking_task, due_second])
+        self.assertEqual([blocking_task, due_second], pick_action.next_actions(tasks, self.namespace))
+
+    def test_priority_of_blocked_task(self):
+        """ Test that a task that blocks a task with a higher priority takes precendence. """
+        tasks = todotxt.Tasks()
+        high_prio_but_blocked = todotxt.Task("(A) Task id:1", tasks=tasks)
+        blocking_task = todotxt.Task("Blocking before:1", tasks=tasks)
+        second_prio = todotxt.Task("(B) Task", tasks=tasks)
+        tasks.extend([high_prio_but_blocked, blocking_task, second_prio])
+        self.assertEqual([blocking_task, second_prio], pick_action.next_actions(tasks, self.namespace))
diff --git a/tests/unittests/todotxt/test_task.py b/tests/unittests/todotxt/test_task.py
index c772674..e99116b 100644
--- a/tests/unittests/todotxt/test_task.py
+++ b/tests/unittests/todotxt/test_task.py
@@ -87,6 +87,34 @@ class TaskPriorityTest(unittest.TestCase):
         self.assertTrue(todotxt.Task("(Z) Task").priority_at_least(None))
         self.assertTrue(todotxt.Task("Task").priority_at_least(None))
 
+    def test_blocking(self):
+        """ Test that the priority of a task without its own priority equals the priority of the task it is
+            blocking. """
+        tasks = todotxt.Tasks()
+        after = todotxt.Task("(A) After id:1", tasks=tasks)
+        before = todotxt.Task("Before before:1", tasks=tasks)
+        tasks.extend([before, after])
+        self.assertEqual("A", before.priority())
+
+    def test_blocking_multiple(self):
+        """ Test that the priority of a task without its own priority equals the highest priority of the tasks it is
+            blocking. """
+        tasks = todotxt.Tasks()
+        after1 = todotxt.Task("(A) After id:1", tasks=tasks)
+        after2 = todotxt.Task("(B) After after:before", tasks=tasks)
+        before = todotxt.Task("Before before:1 id:before", tasks=tasks)
+        tasks.extend([before, after1, after2])
+        self.assertEqual("A", before.priority())
+
+    def test_blocking_completed(self):
+        """ Test that the priority of a completed blocked task is ignored. """
+        tasks = todotxt.Tasks()
+        after = todotxt.Task("(B) After id:1", tasks=tasks)
+        after_completed = todotxt.Task("x (A) After id:2", tasks=tasks)
+        before = todotxt.Task("Before before:1 before:2", tasks=tasks)
+        tasks.extend([before, after, after_completed])
+        self.assertEqual("B", before.priority())
+
 
 class CreationDateTest(unittest.TestCase):
     """ Unit tests for task creation dates. Next-action interprets creation dates in the future as threshold, or