From 951d8ad55c0d54286f9c986257a67dfa9710fcf2 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 22 Apr 2024 03:52:44 +0200 Subject: [PATCH] Simplify code with namedtuples and dataclasses. --- plomtask/http.py | 18 ++++++------- plomtask/processes.py | 34 +++++++++++++++++-------- plomtask/todos.py | 13 +++++++--- templates/day.html | 8 +++--- templates/process.html | 2 +- tests/processes.py | 39 ++++++++--------------------- tests/todos.py | 57 +++++++++++++++++------------------------- 7 files changed, 82 insertions(+), 89 deletions(-) diff --git a/plomtask/http.py b/plomtask/http.py index b5a6c16..d8ca8df 100644 --- a/plomtask/http.py +++ b/plomtask/http.py @@ -1,5 +1,6 @@ """Web server stuff.""" from typing import Any +from collections import namedtuple from http.server import BaseHTTPRequestHandler from http.server import HTTPServer from urllib.parse import urlparse, parse_qs @@ -119,23 +120,22 @@ class TaskHandler(BaseHTTPRequestHandler): def do_GET_day(self) -> dict[str, object]: """Show single Day of ?date=.""" date = self.params.get_str('date', todays_date()) - conditions_listing = [] top_todos = [t for t in Todo.by_date(self.conn, date) if not t.parents] seen_todos: set[int] = set() seen_conditions: set[int] = set() todo_trees = [t.get_step_tree(seen_todos, seen_conditions) for t in top_todos] - for condition in Condition.all(self.conn): - enablers = Todo.enablers_for_at(self.conn, condition, date) - disablers = Todo.disablers_for_at(self.conn, condition, date) - conditions_listing += [{ - 'condition': condition, - 'enablers': enablers, - 'disablers': disablers}] + ConditionsNode = namedtuple('ConditionsNode', + ('condition', 'enablers', 'disablers')) + conditions_nodes: list[ConditionsNode] = [] + for condi in Condition.all(self.conn): + enablers = Todo.enablers_for_at(self.conn, condi, date) + disablers = Todo.disablers_for_at(self.conn, condi, date) + conditions_nodes += [ConditionsNode(condi, enablers, disablers)] return {'day': Day.by_id(self.conn, date, create=True), 'todo_trees': todo_trees, 'processes': Process.all(self.conn), - 'conditions_listing': conditions_listing} + 'conditions_nodes': conditions_nodes} def do_GET_todo(self) -> dict[str, object]: """Show single Todo of ?id=.""" diff --git a/plomtask/processes.py b/plomtask/processes.py index eb43895..32eee4d 100644 --- a/plomtask/processes.py +++ b/plomtask/processes.py @@ -1,12 +1,23 @@ """Collecting Processes and Process-related items.""" from __future__ import annotations -from typing import Any, Set +from dataclasses import dataclass +from typing import Set from plomtask.db import DatabaseConnection, BaseModel from plomtask.misc import VersionedAttribute from plomtask.conditions import Condition, ConditionsRelations from plomtask.exceptions import NotFoundException, BadFormatException +@dataclass +class ProcessStepsNode: + """Collects what's useful to know for ProcessSteps tree display.""" + process: Process + parent_id: int | None + is_explicit: bool + steps: dict[int, ProcessStepsNode] + seen: bool + + class Process(BaseModel, ConditionsRelations): """Template for, and metadata for, Todos, and their arrangements.""" table_name = 'processes' @@ -76,29 +87,30 @@ class Process(BaseModel, ConditionsRelations): return [self.__class__.by_id(db_conn, id_) for id_ in owner_ids] def get_steps(self, db_conn: DatabaseConnection, external_owner: - Process | None = None) -> dict[int, dict[str, object]]: + Process | None = None) -> dict[int, ProcessStepsNode]: """Return tree of depended-on explicit and implicit ProcessSteps.""" - def make_node(step: ProcessStep) -> dict[str, object]: + def make_node(step: ProcessStep) -> ProcessStepsNode: is_explicit = False if external_owner is not None: is_explicit = step.owner_id == external_owner.id_ process = self.__class__.by_id(db_conn, step.step_process_id) step_steps = process.get_steps(db_conn, external_owner) - return {'process': process, 'parent_id': step.parent_step_id, - 'is_explicit': is_explicit, 'steps': step_steps} + return ProcessStepsNode(process, step.parent_step_id, + is_explicit, step_steps, False) - def walk_steps(node_id: int, node: dict[str, Any]) -> None: + def walk_steps(node_id: int, node: ProcessStepsNode) -> None: explicit_children = [s for s in self.explicit_steps if s.parent_step_id == node_id] for child in explicit_children: - node['steps'][child.id_] = make_node(child) - node['seen'] = node_id in seen_step_ids + assert isinstance(child.id_, int) + node.steps[child.id_] = make_node(child) + node.seen = node_id in seen_step_ids seen_step_ids.add(node_id) - for id_, step in node['steps'].items(): + for id_, step in node.steps.items(): walk_steps(id_, step) - steps: dict[int, dict[str, object]] = {} + steps: dict[int, ProcessStepsNode] = {} seen_step_ids: Set[int] = set() if external_owner is None: external_owner = self @@ -124,12 +136,14 @@ class Process(BaseModel, ConditionsRelations): just deleted under its feet), or if the parent step would not be owned by the current Process. """ + def walk_steps(node: ProcessStep) -> None: if node.step_process_id == self.id_: raise BadFormatException('bad step selection causes recursion') step_process = self.by_id(db_conn, node.step_process_id) for step in step_process.explicit_steps: walk_steps(step) + if parent_step_id is not None: try: parent_step = ProcessStep.by_id(db_conn, parent_step_id) diff --git a/plomtask/todos.py b/plomtask/todos.py index cf4c330..ff6cdcb 100644 --- a/plomtask/todos.py +++ b/plomtask/todos.py @@ -1,6 +1,6 @@ """Actionables.""" from __future__ import annotations -from collections import namedtuple +from dataclasses import dataclass from typing import Any from sqlite3 import Row from plomtask.db import DatabaseConnection, BaseModel @@ -10,8 +10,13 @@ from plomtask.exceptions import (NotFoundException, BadFormatException, HandledException) -TodoStepsNode = namedtuple('TodoStepsNode', - ('item', 'is_todo', 'children', 'seen')) +@dataclass +class TodoStepsNode: + """Collects what's useful to know for Todo/Condition tree display.""" + item: Todo | Condition + is_todo: bool + children: list[TodoStepsNode] + seen: bool class Todo(BaseModel, ConditionsRelations): @@ -171,11 +176,13 @@ class Todo(BaseModel, ConditionsRelations): def add_child(self, child: Todo) -> None: """Add child to self.children, avoid recursion, update parenthoods.""" + def walk_steps(node: Todo) -> None: if node.id_ == self.id_: raise BadFormatException('bad child choice causes recursion') for child in node.children: walk_steps(child) + if self.id_ is None: raise HandledException('Can only add children to saved Todos.') if child.id_ is None: diff --git a/templates/day.html b/templates/day.html index a089037..8887f9a 100644 --- a/templates/day.html +++ b/templates/day.html @@ -39,13 +39,13 @@ add todo:

conditions

-{% for node in conditions_listing %} -
  • [{% if node['condition'].is_active %}x{% else %} {% endif %}] {{node['condition'].title.newest|e}} +{% for node in conditions_nodes %} +
  • [{% if node.condition.is_active %}x{% else %} {% endif %}] {{node.condition.title.newest|e}} diff --git a/templates/process.html b/templates/process.html index 85651d7..f8f7baf 100644 --- a/templates/process.html +++ b/templates/process.html @@ -86,7 +86,7 @@ add enables:

    steps

    -{% for step_id, step_node in steps.items() %} +{% for step_node in steps %} {{ step_with_steps(step_id, step_node, 0) }} {% endfor %}
    diff --git a/tests/processes.py b/tests/processes.py index 084cfe0..9413822 100644 --- a/tests/processes.py +++ b/tests/processes.py @@ -1,8 +1,7 @@ """Test Processes module.""" from unittest import TestCase -from typing import Any from tests.utils import TestCaseWithDB, TestCaseWithServer -from plomtask.processes import Process, ProcessStep +from plomtask.processes import Process, ProcessStep, ProcessStepsNode from plomtask.conditions import Condition from plomtask.exceptions import NotFoundException, HandledException @@ -58,46 +57,30 @@ class TestsWithDB(TestCaseWithDB): assert isinstance(self.proc3.id_, int) steps_proc1: list[tuple[int | None, int, int | None]] = [] add_step(self.proc1, steps_proc1, (None, self.proc2.id_, None), 1) - p_1_dict: dict[int, dict[str, Any]] = {1: { - 'process': self.proc2, 'parent_id': None, - 'is_explicit': True, 'steps': {}, 'seen': False - }} + p_1_dict: dict[int, ProcessStepsNode] = {} + p_1_dict[1] = ProcessStepsNode(self.proc2, None, True, {}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) add_step(self.proc1, steps_proc1, (None, self.proc3.id_, None), 2) step_2 = self.proc1.explicit_steps[-1] assert isinstance(step_2.id_, int) - p_1_dict[2] = { - 'process': self.proc3, 'parent_id': None, - 'is_explicit': True, 'steps': {}, 'seen': False - } + p_1_dict[2] = ProcessStepsNode(self.proc3, None, True, {}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) steps_proc2: list[tuple[int | None, int, int | None]] = [] add_step(self.proc2, steps_proc2, (None, self.proc3.id_, None), 3) - p_1_dict[1]['steps'] = {3: { - 'process': self.proc3, 'parent_id': None, - 'is_explicit': False, 'steps': {}, 'seen': False - }} + p_1_dict[1].steps[3] = ProcessStepsNode(self.proc3, None, + False, {}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) add_step(self.proc1, steps_proc1, (None, self.proc2.id_, step_2.id_), 4) - p_1_dict[2]['steps'][4] = { - 'process': self.proc2, 'parent_id': step_2.id_, 'seen': False, - 'is_explicit': True, 'steps': {3: { - 'process': self.proc3, 'parent_id': None, - 'is_explicit': False, 'steps': {}, 'seen': True - }}} + step_3 = ProcessStepsNode(self.proc3, None, False, {}, True) + p_1_dict[2].steps[4] = ProcessStepsNode(self.proc2, step_2.id_, True, + {3: step_3}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) add_step(self.proc1, steps_proc1, (None, self.proc3.id_, 999), 5) - p_1_dict[5] = { - 'process': self.proc3, 'parent_id': None, - 'is_explicit': True, 'steps': {}, 'seen': False - } + p_1_dict[5] = ProcessStepsNode(self.proc3, None, True, {}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) add_step(self.proc1, steps_proc1, (None, self.proc3.id_, 3), 6) - p_1_dict[6] = { - 'process': self.proc3, 'parent_id': None, - 'is_explicit': True, 'steps': {}, 'seen': False - } + p_1_dict[6] = ProcessStepsNode(self.proc3, None, True, {}, False) self.assertEqual(self.proc1.get_steps(self.db_conn, None), p_1_dict) self.assertEqual(self.proc1.used_as_step_by(self.db_conn), diff --git a/tests/todos.py b/tests/todos.py index 4ab34ab..6e88425 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -164,61 +164,50 @@ class TestsWithDB(TestCaseWithDB): todo_1.save(self.db_conn) assert isinstance(todo_1.id_, int) # test minimum - tree_expected = TodoStepsNode(todo_1, True, [], False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_0 = TodoStepsNode(todo_1, True, [], False) + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test non_emtpy seen_todo does something - tree_expected = TodoStepsNode(todo_1, True, [], True) - self.assertEqual(todo_1.get_step_tree({todo_1.id_}, set()), - tree_expected) + node_0.seen = True + self.assertEqual(todo_1.get_step_tree({todo_1.id_}, set()), node_0) # test child shows up todo_2 = Todo(None, self.proc, False, self.date1) todo_2.save(self.db_conn) assert isinstance(todo_2.id_, int) todo_1.add_child(todo_2) - node_todo_2 = TodoStepsNode(todo_2, True, [], False) - tree_expected = TodoStepsNode(todo_1, True, [node_todo_2], False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_2 = TodoStepsNode(todo_2, True, [], False) + node_0.children = [node_2] + node_0.seen = False + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test child shows up with child todo_3 = Todo(None, self.proc, False, self.date1) todo_3.save(self.db_conn) assert isinstance(todo_3.id_, int) todo_2.add_child(todo_3) - node_todo_3 = TodoStepsNode(todo_3, True, [], False) - node_todo_2 = TodoStepsNode(todo_2, True, [node_todo_3], False) - tree_expected = TodoStepsNode(todo_1, True, [node_todo_2], False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_3 = TodoStepsNode(todo_3, True, [], False) + node_2.children = [node_3] + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test same todo can be child-ed multiple times at different locations todo_1.add_child(todo_3) - node_todo_4 = TodoStepsNode(todo_3, True, [], True) - tree_expected = TodoStepsNode(todo_1, True, - [node_todo_2, node_todo_4], False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_4 = TodoStepsNode(todo_3, True, [], True) + node_0.children += [node_4] + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test condition shows up todo_1.set_conditions(self.db_conn, [self.cond1.id_]) - node_cond_1 = TodoStepsNode(self.cond1, False, [], False) - tree_expected = TodoStepsNode(todo_1, True, - [node_todo_2, node_todo_4, node_cond_1], - False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_5 = TodoStepsNode(self.cond1, False, [], False) + node_0.children += [node_5] + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test second condition shows up todo_2.set_conditions(self.db_conn, [self.cond2.id_]) - node_cond_2 = TodoStepsNode(self.cond2, False, [], False) - node_todo_2 = TodoStepsNode(todo_2, True, - [node_todo_3, node_cond_2], False) - tree_expected = TodoStepsNode(todo_1, True, - [node_todo_2, node_todo_4, node_cond_1], - False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_6 = TodoStepsNode(self.cond2, False, [], False) + node_2.children += [node_6] + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test second condition is not hidden if fulfilled by non-sibling todo_1.set_enables(self.db_conn, [self.cond2.id_]) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) # test second condition is hidden if fulfilled by sibling todo_3.set_enables(self.db_conn, [self.cond2.id_]) - node_todo_2 = TodoStepsNode(todo_2, True, [node_todo_3], False) - tree_expected = TodoStepsNode(todo_1, True, - [node_todo_2, node_todo_4, node_cond_1], - False) - self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected) + node_2.children.remove(node_6) + self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) def test_Todo_singularity(self) -> None: """Test pointers made for single object keep pointing to it.""" -- 2.30.2