home · contact · privacy
Overhaul caching.
authorChristian Heller <c.heller@plomlompom.de>
Sat, 15 Jun 2024 05:37:55 +0000 (07:37 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 15 Jun 2024 05:37:55 +0000 (07:37 +0200)
plomtask/days.py
plomtask/db.py
plomtask/http.py
plomtask/processes.py
plomtask/todos.py
tests/conditions.py
tests/processes.py
tests/todos.py
tests/utils.py

index 155ed03aabdbddacdf481267c04aa9d5a0fc29c0..afe4a01be6f509a8b624da7c45650500a96805e2 100644 (file)
@@ -3,7 +3,6 @@ from __future__ import annotations
 from typing import Any
 from sqlite3 import Row
 from datetime import datetime, timedelta
 from typing import Any
 from sqlite3 import Row
 from datetime import datetime, timedelta
-from plomtask.exceptions import HandledException
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.todos import Todo
 from plomtask.dating import (DATE_FORMAT, valid_date)
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.todos import Todo
 from plomtask.dating import (DATE_FORMAT, valid_date)
@@ -14,16 +13,12 @@ class Day(BaseModel[str]):
     table_name = 'days'
     to_save = ['comment']
 
     table_name = 'days'
     to_save = ['comment']
 
-    def __init__(self,
-                 date: str,
-                 comment: str = '',
-                 init_empty_todo_list: bool = False
-                 ) -> None:
+    def __init__(self, date: str, comment: str = '') -> None:
         id_ = valid_date(date)
         super().__init__(id_)
         self.datetime = datetime.strptime(self.date, DATE_FORMAT)
         self.comment = comment
         id_ = valid_date(date)
         super().__init__(id_)
         self.datetime = datetime.strptime(self.date, DATE_FORMAT)
         self.comment = comment
-        self._todos: list[Todo] | None = [] if init_empty_todo_list else None
+        self.todos: list[Todo] = []
 
     def __lt__(self, other: Day) -> bool:
         return self.date < other.date
 
     def __lt__(self, other: Day) -> bool:
         return self.date < other.date
@@ -32,25 +27,22 @@ class Day(BaseModel[str]):
     def from_table_row(cls, db_conn: DatabaseConnection, row: Row | list[Any]
                        ) -> Day:
         """Make from DB row, with linked Todos."""
     def from_table_row(cls, db_conn: DatabaseConnection, row: Row | list[Any]
                        ) -> Day:
         """Make from DB row, with linked Todos."""
-        # pylint: disable=protected-access
-        # (since on ._todo we're only meddling within cls)
         day = super().from_table_row(db_conn, row)
         assert isinstance(day.id_, str)
         day = super().from_table_row(db_conn, row)
         assert isinstance(day.id_, str)
-        day._todos = Todo.by_date(db_conn, day.id_)
+        day.todos = Todo.by_date(db_conn, day.id_)
         return day
 
     @classmethod
     def by_id(cls,
               db_conn: DatabaseConnection, id_: str | None,
               create: bool = False,
         return day
 
     @classmethod
     def by_id(cls,
               db_conn: DatabaseConnection, id_: str | None,
               create: bool = False,
-              init_empty_todo_list: bool = False
               ) -> Day:
               ) -> Day:
-        """Extend BaseModel.by_id with init_empty_todo_list flag."""
-        # pylint: disable=protected-access
-        # (since on ._todo we're only meddling within cls)
+        """Extend BaseModel.by_id checking for new/lost .todos."""
         day = super().by_id(db_conn, id_, create)
         day = super().by_id(db_conn, id_, create)
-        if init_empty_todo_list and day._todos is None:
-            day._todos = []
+        assert day.id_ is not None
+        if day.id_ in Todo.days_to_update:
+            Todo.days_to_update.remove(day.id_)
+            day.todos = Todo.by_date(db_conn, day.id_)
         return day
 
     @classmethod
         return day
 
     @classmethod
@@ -69,16 +61,16 @@ class Day(BaseModel[str]):
             return days
         days.sort()
         if start_date not in [d.date for d in days]:
             return days
         days.sort()
         if start_date not in [d.date for d in days]:
-            days[:] = [Day(start_date, init_empty_todo_list=True)] + days
+            days[:] = [Day(start_date)] + days
         if end_date not in [d.date for d in days]:
         if end_date not in [d.date for d in days]:
-            days += [Day(end_date, init_empty_todo_list=True)]
+            days += [Day(end_date)]
         if len(days) > 1:
             gapless_days = []
             for i, day in enumerate(days):
                 gapless_days += [day]
                 if i < len(days) - 1:
                     while day.next_date != days[i+1].date:
         if len(days) > 1:
             gapless_days = []
             for i, day in enumerate(days):
                 gapless_days += [day]
                 if i < len(days) - 1:
                     while day.next_date != days[i+1].date:
-                        day = Day(day.next_date, init_empty_todo_list=True)
+                        day = Day(day.next_date)
                         gapless_days += [day]
             days[:] = gapless_days
         return days
                         gapless_days += [day]
             days[:] = gapless_days
         return days
@@ -117,14 +109,6 @@ class Day(BaseModel[str]):
         next_datetime = self.datetime + timedelta(days=1)
         return next_datetime.strftime(DATE_FORMAT)
 
         next_datetime = self.datetime + timedelta(days=1)
         return next_datetime.strftime(DATE_FORMAT)
 
-    @property
-    def todos(self) -> list[Todo]:
-        """Return self.todos if initialized, else raise Exception."""
-        if self._todos is None:
-            msg = 'Trying to return from un-initialized Day.todos.'
-            raise HandledException(msg)
-        return list(self._todos)
-
     @property
     def calendarized_todos(self) -> list[Todo]:
         """Return only those of self.todos that have .calendarize set."""
     @property
     def calendarized_todos(self) -> list[Todo]:
         """Return only those of self.todos that have .calendarize set."""
index df98dd0f130bbd75553b2e628cd739d793e98616..99998a6ab29f760ba0d62f90395739dad4b521ff 100644 (file)
@@ -102,9 +102,7 @@ class DatabaseFile:
     @property
     def _user_version(self) -> int:
         """Get DB user_version."""
     @property
     def _user_version(self) -> int:
         """Get DB user_version."""
-        # pylint: disable=protected-access
-        # (since we remain within class)
-        return self.__class__._get_version_of_db(self.path)
+        return self._get_version_of_db(self.path)
 
     def _validate_schema(self) -> None:
         """Compare found schema with what's stored at PATH_DB_SCHEMA."""
 
     def _validate_schema(self) -> None:
         """Compare found schema with what's stored at PATH_DB_SCHEMA."""
@@ -240,6 +238,7 @@ class BaseModel(Generic[BaseModelId]):
     id_: None | BaseModelId
     cache_: dict[BaseModelId, Self]
     to_search: list[str] = []
     id_: None | BaseModelId
     cache_: dict[BaseModelId, Self]
     to_search: list[str] = []
+    _exists = True
 
     def __init__(self, id_: BaseModelId | None) -> None:
         if isinstance(id_, int) and id_ < 1:
 
     def __init__(self, id_: BaseModelId | None) -> None:
         if isinstance(id_, int) and id_ < 1:
@@ -273,18 +272,26 @@ class BaseModel(Generic[BaseModelId]):
         return self.id_ < other.id_
 
     # cache management
         return self.id_ < other.id_
 
     # cache management
-
-    @classmethod
-    def _get_cached(cls: type[BaseModelInstance],
-                    id_: BaseModelId) -> BaseModelInstance | None:
-        """Get object of id_ from class's cache, or None if not found."""
-        # pylint: disable=consider-iterating-dictionary
-        cache = cls.get_cache()
-        if id_ in cache.keys():
-            obj = cache[id_]
-            assert isinstance(obj, cls)
-            return obj
-        return None
+    # (we primarily use the cache to ensure we work on the same object in
+    # memory no matter where and how we retrieve it, e.g. we don't want
+    # .by_id() calls to create a new object each time, but rather a pointer
+    # to the one already instantiated)
+
+    def __getattribute__(self, name: str) -> Any:
+        """Ensure fail if ._disappear() was called, except to check ._exists"""
+        if name != '_exists' and not super().__getattribute__('_exists'):
+            raise HandledException('Object does not exist.')
+        return super().__getattribute__(name)
+
+    def _disappear(self) -> None:
+        """Invalidate object, make future use raise exceptions."""
+        assert self.id_ is not None
+        if self._get_cached(self.id_):
+            self._uncache()
+        to_kill = list(self.__dict__.keys())
+        for attr in to_kill:
+            delattr(self, attr)
+        self._exists = False
 
     @classmethod
     def empty_cache(cls) -> None:
 
     @classmethod
     def empty_cache(cls) -> None:
@@ -299,18 +306,40 @@ class BaseModel(Generic[BaseModelId]):
             cls.cache_ = d
         return cls.cache_
 
             cls.cache_ = d
         return cls.cache_
 
-    def cache(self) -> None:
-        """Update object in class's cache."""
+    @classmethod
+    def _get_cached(cls: type[BaseModelInstance],
+                    id_: BaseModelId) -> BaseModelInstance | None:
+        """Get object of id_ from class's cache, or None if not found."""
+        # pylint: disable=consider-iterating-dictionary
+        cache = cls.get_cache()
+        if id_ in cache.keys():
+            obj = cache[id_]
+            assert isinstance(obj, cls)
+            return obj
+        return None
+
+    def _cache(self) -> None:
+        """Update object in class's cache.
+
+        Also calls ._disappear if cache holds older reference to object of same
+        ID, but different memory address, to avoid doing anything with
+        dangling leftovers.
+        """
         if self.id_ is None:
             raise HandledException('Cannot cache object without ID.')
         if self.id_ is None:
             raise HandledException('Cannot cache object without ID.')
-        cache = self.__class__.get_cache()
+        cache = self.get_cache()
+        old_cached = self._get_cached(self.id_)
+        if old_cached and id(old_cached) != id(self):
+            # pylint: disable=protected-access
+            # (cause we remain within the class)
+            old_cached._disappear()
         cache[self.id_] = self
 
         cache[self.id_] = self
 
-    def uncache(self) -> None:
+    def _uncache(self) -> None:
         """Remove self from cache."""
         if self.id_ is None:
             raise HandledException('Cannot un-cache object without ID.')
         """Remove self from cache."""
         if self.id_ is None:
             raise HandledException('Cannot un-cache object without ID.')
-        cache = self.__class__.get_cache()
+        cache = self.get_cache()
         del cache[self.id_]
 
     # object retrieval and generation
         del cache[self.id_]
 
     # object retrieval and generation
@@ -320,9 +349,9 @@ class BaseModel(Generic[BaseModelId]):
                        # pylint: disable=unused-argument
                        db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> BaseModelInstance:
                        # pylint: disable=unused-argument
                        db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> BaseModelInstance:
-        """Make from DB row, write to DB cache."""
+        """Make from DB row, update DB cache with it."""
         obj = cls(*row)
         obj = cls(*row)
-        obj.cache()
+        obj._cache()
         return obj
 
     @classmethod
         return obj
 
     @classmethod
@@ -343,7 +372,6 @@ class BaseModel(Generic[BaseModelId]):
             if not obj:
                 for row in db_conn.row_where(cls.table_name, 'id', id_):
                     obj = cls.from_table_row(db_conn, row)
             if not obj:
                 for row in db_conn.row_where(cls.table_name, 'id', id_):
                     obj = cls.from_table_row(db_conn, row)
-                    obj.cache()
                     break
         if obj:
             return obj
                     break
         if obj:
             return obj
@@ -437,7 +465,7 @@ class BaseModel(Generic[BaseModelId]):
                                       values)
         if not isinstance(self.id_, str):
             self.id_ = cursor.lastrowid  # type: ignore[assignment]
                                       values)
         if not isinstance(self.id_, str):
             self.id_ = cursor.lastrowid  # type: ignore[assignment]
-        self.cache()
+        self._cache()
         for attr_name in self.to_save_versioned:
             getattr(self, attr_name).save(db_conn)
         for table, column, attr_name, key_index in self.to_save_relations:
         for attr_name in self.to_save_versioned:
             getattr(self, attr_name).save(db_conn)
         for table, column, attr_name, key_index in self.to_save_relations:
@@ -448,13 +476,12 @@ class BaseModel(Generic[BaseModelId]):
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB and cache, including dependencies."""
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB and cache, including dependencies."""
-        # pylint: disable=protected-access
-        # (since we remain within class)
-        if self.id_ is None or self.__class__._get_cached(self.id_) is None:
+        if self.id_ is None or self._get_cached(self.id_) is None:
             raise HandledException('cannot remove unsaved item')
         for attr_name in self.to_save_versioned:
             getattr(self, attr_name).remove(db_conn)
         for table, column, attr_name, _ in self.to_save_relations:
             db_conn.delete_where(table, column, self.id_)
             raise HandledException('cannot remove unsaved item')
         for attr_name in self.to_save_versioned:
             getattr(self, attr_name).remove(db_conn)
         for table, column, attr_name, _ in self.to_save_relations:
             db_conn.delete_where(table, column, self.id_)
-        self.uncache()
+        self._uncache()
         db_conn.delete_where(self.table_name, 'id', self.id_)
         db_conn.delete_where(self.table_name, 'id', self.id_)
+        self._disappear()
index 230ed3f26c5d53398a27d87558a28abeb28ceff5..fc0059c530e20c5e347a264703905b3fcadcb3df 100644 (file)
@@ -139,8 +139,11 @@ class TaskHandler(BaseHTTPRequestHandler):
                         msg = f'{not_found_msg}: {self._site}'
                         raise NotFoundException(msg)
                 except HandledException as error:
                         msg = f'{not_found_msg}: {self._site}'
                         raise NotFoundException(msg)
                 except HandledException as error:
-                    html = self.server.jinja.\
-                            get_template('msg.html').render(msg=error)
+                    for cls in (Day, Todo, Condition, Process, ProcessStep):
+                        assert hasattr(cls, 'empty_cache')
+                        cls.empty_cache()
+                    tmpl = self.server.jinja.get_template('msg.html')
+                    html = tmpl.render(msg=error)
                     self._send_html(html, error.http_code)
                 finally:
                     self.conn.close()
                     self._send_html(html, error.http_code)
                 finally:
                     self.conn.close()
@@ -205,8 +208,7 @@ class TaskHandler(BaseHTTPRequestHandler):
     def do_GET_day(self) -> dict[str, object]:
         """Show single Day of ?date=."""
         date = self._params.get_str('date', date_in_n_days(0))
     def do_GET_day(self) -> dict[str, object]:
         """Show single Day of ?date=."""
         date = self._params.get_str('date', date_in_n_days(0))
-        day = Day.by_id(self.conn, date, create=True,
-                        init_empty_todo_list=True)
+        day = Day.by_id(self.conn, date, create=True)
         make_type = self._params.get_str('make_type')
         conditions_present = []
         enablers_for = {}
         make_type = self._params.get_str('make_type')
         conditions_present = []
         enablers_for = {}
@@ -482,10 +484,6 @@ class TaskHandler(BaseHTTPRequestHandler):
             if len(efforts) > 0:
                 todo.effort = float(efforts[i]) if efforts[i] else None
             todo.save(self.conn)
             if len(efforts) > 0:
                 todo.effort = float(efforts[i]) if efforts[i] else None
             todo.save(self.conn)
-            for condition in todo.enables:
-                condition.save(self.conn)
-            for condition in todo.disables:
-                condition.save(self.conn)
         return f'/day?date={date}&make_type={make_type}'
 
     def do_POST_todo(self) -> str:
         return f'/day?date={date}&make_type={make_type}'
 
     def do_POST_todo(self) -> str:
@@ -541,10 +539,6 @@ class TaskHandler(BaseHTTPRequestHandler):
         todo.calendarize = len(self._form_data.get_all_str('calendarize')) > 0
         todo.comment = self._form_data.get_str('comment', ignore_strict=True)
         todo.save(self.conn)
         todo.calendarize = len(self._form_data.get_all_str('calendarize')) > 0
         todo.comment = self._form_data.get_str('comment', ignore_strict=True)
         todo.save(self.conn)
-        for condition in todo.enables:
-            condition.save(self.conn)
-        for condition in todo.disables:
-            condition.save(self.conn)
         return f'/todo?id={todo.id_}'
 
     def do_POST_process_descriptions(self) -> str:
         return f'/todo?id={todo.id_}'
 
     def do_POST_process_descriptions(self) -> str:
@@ -606,12 +600,10 @@ class TaskHandler(BaseHTTPRequestHandler):
                                       None)]
             except ValueError:
                 new_step_title = step_identifier
                                       None)]
             except ValueError:
                 new_step_title = step_identifier
-        process.uncache()
         process.set_steps(self.conn, steps)
         process.set_step_suppressions(self.conn,
                                       self._form_data.
                                       get_all_int('suppresses'))
         process.set_steps(self.conn, steps)
         process.set_step_suppressions(self.conn,
                                       self._form_data.
                                       get_all_int('suppresses'))
-        process.save(self.conn)
         owners_to_set = []
         new_owner_title = None
         for owner_identifier in self._form_data.get_all_str('step_of'):
         owners_to_set = []
         new_owner_title = None
         for owner_identifier in self._form_data.get_all_str('step_of'):
@@ -627,6 +619,7 @@ class TaskHandler(BaseHTTPRequestHandler):
         elif new_owner_title:
             title_b64_encoded = b64encode(new_owner_title.encode()).decode()
             params = f'has_step={process.id_}&title_b64={title_b64_encoded}'
         elif new_owner_title:
             title_b64_encoded = b64encode(new_owner_title.encode()).decode()
             params = f'has_step={process.id_}&title_b64={title_b64_encoded}'
+        process.save(self.conn)
         return f'/process?{params}'
 
     def do_POST_condition_descriptions(self) -> str:
         return f'/process?{params}'
 
     def do_POST_condition_descriptions(self) -> str:
index 8082c3cb1d412fa7aa7806735e20ee908f27c404..06ee4ba9b9c2a3011019b03b1b0e21633fce780c 100644 (file)
@@ -50,21 +50,12 @@ class Process(BaseModel[int], ConditionsRelations):
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Process:
         """Make from DB row, with dependencies."""
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Process:
         """Make from DB row, with dependencies."""
-        # pylint: disable=no-member
         process = super().from_table_row(db_conn, row)
         assert isinstance(process.id_, int)
         for name in ('title', 'description', 'effort'):
             table = f'process_{name}s'
             for row_ in db_conn.row_where(table, 'parent', process.id_):
                 getattr(process, name).history_from_row(row_)
         process = super().from_table_row(db_conn, row)
         assert isinstance(process.id_, int)
         for name in ('title', 'description', 'effort'):
             table = f'process_{name}s'
             for row_ in db_conn.row_where(table, 'parent', process.id_):
                 getattr(process, name).history_from_row(row_)
-        for row_ in db_conn.row_where('process_steps', 'owner',
-                                      process.id_):
-            step = ProcessStep.from_table_row(db_conn, row_)
-            process.explicit_steps += [step]
-        for row_ in db_conn.row_where('process_step_suppressions', 'process',
-                                      process.id_):
-            step = ProcessStep.by_id(db_conn, row_[1])
-            process.suppressed_steps += [step]
         for name in ('conditions', 'blockers', 'enables', 'disables'):
             table = f'process_{name}'
             assert isinstance(process.id_, int)
         for name in ('conditions', 'blockers', 'enables', 'disables'):
             table = f'process_{name}'
             assert isinstance(process.id_, int)
@@ -72,6 +63,13 @@ class Process(BaseModel[int], ConditionsRelations):
                                              'process', process.id_):
                 target = getattr(process, name)
                 target += [Condition.by_id(db_conn, c_id)]
                                              'process', process.id_):
                 target = getattr(process, name)
                 target += [Condition.by_id(db_conn, c_id)]
+        for row_ in db_conn.row_where('process_steps', 'owner', process.id_):
+            step = ProcessStep.from_table_row(db_conn, row_)
+            process.explicit_steps += [step]
+        for row_ in db_conn.row_where('process_step_suppressions', 'process',
+                                      process.id_):
+            step = ProcessStep.by_id(db_conn, row_[1])
+            process.suppressed_steps += [step]
         process.n_owners = len(process.used_as_step_by(db_conn))
         return process
 
         process.n_owners = len(process.used_as_step_by(db_conn))
         return process
 
@@ -149,12 +147,9 @@ class Process(BaseModel[int], ConditionsRelations):
                 walk_steps(step)
 
         assert isinstance(self.id_, int)
                 walk_steps(step)
 
         assert isinstance(self.id_, int)
-        for step in self.explicit_steps:
-            step.uncache()
-        self.explicit_steps = []
-        db_conn.delete_where('process_steps', 'owner', self.id_)
-        for step in steps:
-            step.save(db_conn)
+        for step in [s for s in self.explicit_steps if s not in steps]:
+            step.remove(db_conn)
+        for step in [s for s in steps if s not in self.explicit_steps]:
             if step.parent_step_id is not None:
                 try:
                     parent_step = ProcessStep.by_id(db_conn,
             if step.parent_step_id is not None:
                 try:
                     parent_step = ProcessStep.by_id(db_conn,
@@ -164,7 +159,7 @@ class Process(BaseModel[int], ConditionsRelations):
                 except NotFoundException:
                     step.parent_step_id = None
             walk_steps(step)
                 except NotFoundException:
                     step.parent_step_id = None
             walk_steps(step)
-            self.explicit_steps += [step]
+            step.save(db_conn)
 
     def set_owners(self, db_conn: DatabaseConnection,
                    owner_ids: list[int]) -> None:
 
     def set_owners(self, db_conn: DatabaseConnection,
                    owner_ids: list[int]) -> None:
@@ -222,6 +217,16 @@ class ProcessStep(BaseModel[int]):
         self.step_process_id = step_process_id
         self.parent_step_id = parent_step_id
 
         self.step_process_id = step_process_id
         self.parent_step_id = parent_step_id
 
+    def save(self, db_conn: DatabaseConnection) -> None:
+        """Remove from DB, and owner's .explicit_steps."""
+        super().save(db_conn)
+        owner = Process.by_id(db_conn, self.owner_id)
+        if self not in owner.explicit_steps:
+            for s in [s for s in owner.explicit_steps if s.id_ == self.id_]:
+                s.remove(db_conn)
+            owner.explicit_steps += [self]
+        owner.explicit_steps.sort(key=hash)
+
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, and owner's .explicit_steps."""
         owner = Process.by_id(db_conn, self.owner_id)
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, and owner's .explicit_steps."""
         owner = Process.by_id(db_conn, self.owner_id)
index fa009b12b8fb64e42b8a42f728fd0d4fb1740367..705bd725e2ff662ab4f9f2e370e61169a413ff03 100644 (file)
@@ -1,7 +1,7 @@
 """Actionables."""
 from __future__ import annotations
 from dataclasses import dataclass
 """Actionables."""
 from __future__ import annotations
 from dataclasses import dataclass
-from typing import Any
+from typing import Any, Set
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.processes import Process, ProcessStepsNode
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.processes import Process, ProcessStepsNode
@@ -34,6 +34,9 @@ class Todo(BaseModel[int], ConditionsRelations):
                          ('todo_children', 'parent', 'children', 0),
                          ('todo_children', 'child', 'parents', 1)]
     to_search = ['comment']
                          ('todo_children', 'parent', 'children', 0),
                          ('todo_children', 'child', 'parents', 1)]
     to_search = ['comment']
+    days_to_update: Set[str] = set()
+    children: list[Todo]
+    parents: list[Todo]
 
     # pylint: disable=too-many-arguments
     def __init__(self, id_: int | None,
 
     # pylint: disable=too-many-arguments
     def __init__(self, id_: int | None,
@@ -51,8 +54,8 @@ class Todo(BaseModel[int], ConditionsRelations):
         self.date = valid_date(date)
         self.comment = comment
         self.effort = effort
         self.date = valid_date(date)
         self.comment = comment
         self.effort = effort
-        self.children: list[Todo] = []
-        self.parents: list[Todo] = []
+        self.children = []
+        self.parents = []
         self.calendarize = calendarize
         if not self.id_:
             self.calendarize = self.process.calendarize
         self.calendarize = calendarize
         if not self.id_:
             self.calendarize = self.process.calendarize
@@ -129,11 +132,9 @@ class Todo(BaseModel[int], ConditionsRelations):
         assert isinstance(todo.id_, int)
         for t_id in db_conn.column_where('todo_children', 'child',
                                          'parent', todo.id_):
         assert isinstance(todo.id_, int)
         for t_id in db_conn.column_where('todo_children', 'child',
                                          'parent', todo.id_):
-            # pylint: disable=no-member
             todo.children += [cls.by_id(db_conn, t_id)]
         for t_id in db_conn.column_where('todo_children', 'parent',
                                          'child', todo.id_):
             todo.children += [cls.by_id(db_conn, t_id)]
         for t_id in db_conn.column_where('todo_children', 'parent',
                                          'child', todo.id_):
-            # pylint: disable=no-member
             todo.parents += [cls.by_id(db_conn, t_id)]
         for name in ('conditions', 'blockers', 'enables', 'disables'):
             table = f'todo_{name}'
             todo.parents += [cls.by_id(db_conn, t_id)]
         for name in ('conditions', 'blockers', 'enables', 'disables'):
             table = f'todo_{name}'
@@ -297,12 +298,17 @@ class Todo(BaseModel[int], ConditionsRelations):
         if self.effort and self.effort < 0 and self.is_deletable:
             self.remove(db_conn)
             return
         if self.effort and self.effort < 0 and self.is_deletable:
             self.remove(db_conn)
             return
+        if self.id_ is None:
+            self.__class__.days_to_update.add(self.date)
         super().save(db_conn)
         super().save(db_conn)
+        for condition in self.enables + self.disables + self.conditions:
+            condition.save(db_conn)
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, including relations."""
         if not self.is_deletable:
             raise HandledException('Cannot remove non-deletable Todo.')
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, including relations."""
         if not self.is_deletable:
             raise HandledException('Cannot remove non-deletable Todo.')
+        self.__class__.days_to_update.add(self.date)
         children_to_remove = self.children[:]
         parents_to_remove = self.parents[:]
         for child in children_to_remove:
         children_to_remove = self.children[:]
         parents_to_remove = self.parents[:]
         for child in children_to_remove:
index c9b516418f73bf3700f213bc36d0c00235bab5dc..3b05bd098da61ecbeec25588bd5e76572bde5ef2 100644 (file)
@@ -44,13 +44,13 @@ class TestsWithDB(TestCaseWithDB):
     def test_Condition_remove(self) -> None:
         """Test .remove() effects on DB and cache."""
         self.check_remove()
     def test_Condition_remove(self) -> None:
         """Test .remove() effects on DB and cache."""
         self.check_remove()
-        c = Condition(None)
         proc = Process(None)
         proc.save(self.db_conn)
         todo = Todo(None, proc, False, '2024-01-01')
         for depender in (proc, todo):
             assert hasattr(depender, 'save')
             assert hasattr(depender, 'set_conditions')
         proc = Process(None)
         proc.save(self.db_conn)
         todo = Todo(None, proc, False, '2024-01-01')
         for depender in (proc, todo):
             assert hasattr(depender, 'save')
             assert hasattr(depender, 'set_conditions')
+            c = Condition(None)
             c.save(self.db_conn)
             depender.save(self.db_conn)
             depender.set_conditions(self.db_conn, [c.id_], 'conditions')
             c.save(self.db_conn)
             depender.save(self.db_conn)
             depender.set_conditions(self.db_conn, [c.id_], 'conditions')
index e374c3b0fb28f2343e4e3c0fd6fdc8c4b6fed1fb..34f6427e4d06152a17f035672061757cce203cac 100644 (file)
@@ -58,7 +58,6 @@ class TestsWithDB(TestCaseWithDB):
     def test_Process_conditions_saving(self) -> None:
         """Test .save/.save_core."""
         p, set1, set2, set3 = self.p_of_conditions()
     def test_Process_conditions_saving(self) -> None:
         """Test .save/.save_core."""
         p, set1, set2, set3 = self.p_of_conditions()
-        p.uncache()
         r = Process.by_id(self.db_conn, p.id_)
         self.assertEqual(sorted(r.conditions), sorted(set1))
         self.assertEqual(sorted(r.enables), sorted(set2))
         r = Process.by_id(self.db_conn, p.id_)
         self.assertEqual(sorted(r.conditions), sorted(set1))
         self.assertEqual(sorted(r.enables), sorted(set2))
@@ -75,7 +74,6 @@ class TestsWithDB(TestCaseWithDB):
         assert isinstance(p.id_, int)
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', p.id_):
         assert isinstance(p.id_, int)
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', p.id_):
-            # pylint: disable=no-member
             r = Process.from_table_row(self.db_conn, row)
             self.assertEqual(sorted(r.conditions), sorted(set1))
             self.assertEqual(sorted(r.enables), sorted(set2))
             r = Process.from_table_row(self.db_conn, row)
             self.assertEqual(sorted(r.conditions), sorted(set1))
             self.assertEqual(sorted(r.enables), sorted(set2))
@@ -116,7 +114,8 @@ class TestsWithDB(TestCaseWithDB):
         s_p2_to_p1_first = ProcessStep(None, p1.id_, p2.id_, s_p3_to_p1.id_)
         steps_p1 += [s_p2_to_p1_first]
         p1.set_steps(self.db_conn, steps_p1)
         s_p2_to_p1_first = ProcessStep(None, p1.id_, p2.id_, s_p3_to_p1.id_)
         steps_p1 += [s_p2_to_p1_first]
         p1.set_steps(self.db_conn, steps_p1)
-        seen_3 = ProcessStepsNode(p3, None, False, {}, True)
+        seen_3 = ProcessStepsNode(p3, None, False, {}, False)
+        p1_dict[1].steps[3].seen = True
         p1_dict[2].steps[4] = ProcessStepsNode(p2, s_p3_to_p1.id_, True,
                                                {3: seen_3})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         p1_dict[2].steps[4] = ProcessStepsNode(p2, s_p3_to_p1.id_, True,
                                                {3: seen_3})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
@@ -150,9 +149,9 @@ class TestsWithDB(TestCaseWithDB):
         s_p3_to_p2_first = ProcessStep(None, p2.id_, p3.id_, s_p3_to_p2.id_)
         steps_p2 += [s_p3_to_p2_first]
         p2.set_steps(self.db_conn, steps_p2)
         s_p3_to_p2_first = ProcessStep(None, p2.id_, p3.id_, s_p3_to_p2.id_)
         steps_p2 += [s_p3_to_p2_first]
         p2.set_steps(self.db_conn, steps_p2)
-        p1_dict[1].steps[3].steps[7] = ProcessStepsNode(p3, 3, False, {})
+        p1_dict[1].steps[3].steps[7] = ProcessStepsNode(p3, 3, False, {}, True)
         p1_dict[2].steps[4].steps[3].steps[7] = ProcessStepsNode(p3, 3, False,
         p1_dict[2].steps[4].steps[3].steps[7] = ProcessStepsNode(p3, 3, False,
-                                                                 {}, True)
+                                                                 {}, False)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         # ensure suppressed step nodes are hidden
         assert isinstance(s_p3_to_p2.id_, int)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         # ensure suppressed step nodes are hidden
         assert isinstance(s_p3_to_p2.id_, int)
@@ -208,17 +207,19 @@ class TestsWithDB(TestCaseWithDB):
         assert isinstance(p3.id_, int)
         step = ProcessStep(None, p2.id_, p1.id_, None)
         p2.set_steps(self.db_conn, [step])
         assert isinstance(p3.id_, int)
         step = ProcessStep(None, p2.id_, p1.id_, None)
         p2.set_steps(self.db_conn, [step])
+        step_id = step.id_
         with self.assertRaises(HandledException):
             p1.remove(self.db_conn)
         p2.set_steps(self.db_conn, [])
         with self.assertRaises(NotFoundException):
         with self.assertRaises(HandledException):
             p1.remove(self.db_conn)
         p2.set_steps(self.db_conn, [])
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, step.id_)
+            ProcessStep.by_id(self.db_conn, step_id)
         p1.remove(self.db_conn)
         step = ProcessStep(None, p2.id_, p3.id_, None)
         p1.remove(self.db_conn)
         step = ProcessStep(None, p2.id_, p3.id_, None)
+        step_id = step.id_
         p2.set_steps(self.db_conn, [step])
         p2.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
         p2.set_steps(self.db_conn, [step])
         p2.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, step.id_)
+            ProcessStep.by_id(self.db_conn, step_id)
         todo = Todo(None, p3, False, '2024-01-01')
         todo.save(self.db_conn)
         with self.assertRaises(HandledException):
         todo = Todo(None, p3, False, '2024-01-01')
         todo.save(self.db_conn)
         with self.assertRaises(HandledException):
@@ -233,13 +234,16 @@ class TestsWithDBForProcessStep(TestCaseWithDB):
     default_init_kwargs = {'owner_id': 2, 'step_process_id': 3,
                            'parent_step_id': 4}
 
     default_init_kwargs = {'owner_id': 2, 'step_process_id': 3,
                            'parent_step_id': 4}
 
-    def test_ProcessStep_from_table_row(self) -> None:
-        """Test .from_table_row() properly reads in class from DB"""
-        self.check_from_table_row(2, 3, None)
+    def setUp(self) -> None:
+        super().setUp()
+        p = Process(1)
+        p.save(self.db_conn)
+        p = Process(2)
+        p.save(self.db_conn)
 
 
-    def test_ProcessStep_singularity(self) -> None:
-        """Test pointers made for single object keep pointing to it."""
-        self.check_singularity('parent_step_id', 1, 2, 3, None)
+    def test_saving_and_caching(self) -> None:
+        """Test storage and initialization of instances and attributes."""
+        self.check_saving_and_caching(id_=1, **self.default_init_kwargs)
 
     def test_ProcessStep_remove(self) -> None:
         """Test .remove and unsetting of owner's .explicit_steps entry."""
 
     def test_ProcessStep_remove(self) -> None:
         """Test .remove and unsetting of owner's .explicit_steps entry."""
@@ -297,6 +301,7 @@ class TestsWithServer(TestCaseWithServer):
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         retrieved_step = retrieved_process.explicit_steps[0]
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         retrieved_step = retrieved_process.explicit_steps[0]
+        retrieved_step_id = retrieved_step.id_
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
@@ -307,7 +312,7 @@ class TestsWithServer(TestCaseWithServer):
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
         with self.assertRaises(NotFoundException):
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, retrieved_step.id_)
+            ProcessStep.by_id(self.db_conn, retrieved_step_id)
         # post new first (top_level) step of process 3 to process 1
         form_data_1['new_top_step'] = [3]
         self.post_process(1, form_data_1)
         # post new first (top_level) step of process 3 to process 1
         form_data_1['new_top_step'] = [3]
         self.post_process(1, form_data_1)
@@ -324,14 +329,14 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(retrieved_process.explicit_steps, [])
         # post to process empty steps list but keep, expect 400
         form_data_1['steps'] = []
         self.assertEqual(retrieved_process.explicit_steps, [])
         # post to process empty steps list but keep, expect 400
         form_data_1['steps'] = []
-        form_data_1['keep_step'] = [retrieved_step.id_]
+        form_data_1['keep_step'] = [retrieved_step_id]
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         # post to process steps list with keep on non-created step, expect 400
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         # post to process steps list with keep on non-created step, expect 400
-        form_data_1['steps'] = [retrieved_step.id_]
-        form_data_1['keep_step'] = [retrieved_step.id_]
+        form_data_1['steps'] = [retrieved_step_id]
+        form_data_1['keep_step'] = [retrieved_step_id]
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         # post to process steps list with keep and process ID, expect 200
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         # post to process steps list with keep and process ID, expect 200
-        form_data_1[f'step_{retrieved_step.id_}_process_id'] = [2]
+        form_data_1[f'step_{retrieved_step_id}_process_id'] = [2]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
@@ -357,11 +362,11 @@ class TestsWithServer(TestCaseWithServer):
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 2)
         retrieved_step_0 = retrieved_process.explicit_steps[0]
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 2)
         retrieved_step_0 = retrieved_process.explicit_steps[0]
-        self.assertEqual(retrieved_step_0.step_process_id, 2)
+        self.assertEqual(retrieved_step_0.step_process_id, 3)
         self.assertEqual(retrieved_step_0.owner_id, 1)
         self.assertEqual(retrieved_step_0.parent_step_id, None)
         retrieved_step_1 = retrieved_process.explicit_steps[1]
         self.assertEqual(retrieved_step_0.owner_id, 1)
         self.assertEqual(retrieved_step_0.parent_step_id, None)
         retrieved_step_1 = retrieved_process.explicit_steps[1]
-        self.assertEqual(retrieved_step_1.step_process_id, 3)
+        self.assertEqual(retrieved_step_1.step_process_id, 2)
         self.assertEqual(retrieved_step_1.owner_id, 1)
         self.assertEqual(retrieved_step_1.parent_step_id, None)
         # post to process steps list with keeps etc., but trigger recursion
         self.assertEqual(retrieved_step_1.owner_id, 1)
         self.assertEqual(retrieved_step_1.parent_step_id, None)
         # post to process steps list with keeps etc., but trigger recursion
@@ -382,9 +387,9 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(retrieved_step_1.step_process_id, 3)
         self.assertEqual(retrieved_step_1.owner_id, 1)
         self.assertEqual(retrieved_step_1.parent_step_id, None)
         self.assertEqual(retrieved_step_1.step_process_id, 3)
         self.assertEqual(retrieved_step_1.owner_id, 1)
         self.assertEqual(retrieved_step_1.parent_step_id, None)
-        form_data_1[f'step_{retrieved_step_1.id_}_process_id'] = [3]
         # post sub-step to step
         # post sub-step to step
-        form_data_1[f'new_step_to_{retrieved_step_1.id_}'] = [3]
+        form_data_1[f'step_{retrieved_step_0.id_}_process_id'] = [3]
+        form_data_1[f'new_step_to_{retrieved_step_0.id_}'] = [3]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 3)
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 3)
index 1a9eab61c0f77932c11b24ebae15cfc9a982287b..9317c398b255c51b14046f888b3de4a24b70238d 100644 (file)
@@ -187,9 +187,9 @@ class TestsWithDB(TestCaseWithDB):
         todo_2 = Todo.create_with_children(self.db_conn, proc2.id_, self.date1)
         self.assertEqual(3, len(todo_2.children))
         self.assertEqual(todo_1, todo_2.children[0])
         todo_2 = Todo.create_with_children(self.db_conn, proc2.id_, self.date1)
         self.assertEqual(3, len(todo_2.children))
         self.assertEqual(todo_1, todo_2.children[0])
-        self.assertEqual(self.proc, todo_2.children[1].process)
-        self.assertEqual(proc3, todo_2.children[2].process)
-        todo_3 = todo_2.children[2]
+        self.assertEqual(self.proc, todo_2.children[2].process)
+        self.assertEqual(proc3, todo_2.children[1].process)
+        todo_3 = todo_2.children[1]
         self.assertEqual(len(todo_3.children), 1)
         self.assertEqual(todo_3.children[0].process, proc4)
 
         self.assertEqual(len(todo_3.children), 1)
         self.assertEqual(todo_3.children[0].process, proc4)
 
@@ -207,9 +207,10 @@ class TestsWithDB(TestCaseWithDB):
         todo_2 = Todo(None, self.proc, False, self.date1)
         todo_2.save(self.db_conn)
         todo_1.add_child(todo_2)
         todo_2 = Todo(None, self.proc, False, self.date1)
         todo_2.save(self.db_conn)
         todo_1.add_child(todo_2)
+        todo_1_id = todo_1.id_
         todo_1.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
         todo_1.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
-            Todo.by_id(self.db_conn, todo_1.id_)
+            Todo.by_id(self.db_conn, todo_1_id)
         self.assertEqual(todo_0.children, [])
         self.assertEqual(todo_2.parents, [])
         todo_2.comment = 'foo'
         self.assertEqual(todo_0.children, [])
         self.assertEqual(todo_2.parents, [])
         todo_2.comment = 'foo'
@@ -229,9 +230,10 @@ class TestsWithDB(TestCaseWithDB):
         todo_1.save(self.db_conn)
         Todo.by_id(self.db_conn, todo_1.id_)
         todo_1.comment = ''
         todo_1.save(self.db_conn)
         Todo.by_id(self.db_conn, todo_1.id_)
         todo_1.comment = ''
+        todo_1_id = todo_1.id_
         todo_1.save(self.db_conn)
         with self.assertRaises(NotFoundException):
         todo_1.save(self.db_conn)
         with self.assertRaises(NotFoundException):
-            Todo.by_id(self.db_conn, todo_1.id_)
+            Todo.by_id(self.db_conn, todo_1_id)
 
 
 class TestsWithServer(TestCaseWithServer):
 
 
 class TestsWithServer(TestCaseWithServer):
@@ -397,7 +399,7 @@ class TestsWithServer(TestCaseWithServer):
 
     def test_do_POST_day_todo_doneness(self) -> None:
         """Test Todo doneness can be posted to Day view."""
 
     def test_do_POST_day_todo_doneness(self) -> None:
         """Test Todo doneness can be posted to Day view."""
-        form_data = self.post_process()
+        self.post_process()
         form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'}
         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
         form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'}
         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
index 6581c61a1546e952c08b2669d28efdc0e27a06ee..a9a4e80418a54f288281999b964ce8364474c177 100644 (file)
@@ -77,6 +77,7 @@ class TestCaseWithDB(TestCase):
         for item in content:
             expected_cache[item.id_] = item
         self.assertEqual(self.checked_class.get_cache(), expected_cache)
         for item in content:
             expected_cache[item.id_] = item
         self.assertEqual(self.checked_class.get_cache(), expected_cache)
+        hashes_content = [hash(x) for x in content]
         db_found: list[Any] = []
         for item in content:
             assert isinstance(item.id_, type(self.default_ids[0]))
         db_found: list[Any] = []
         for item in content:
             assert isinstance(item.id_, type(self.default_ids[0]))
@@ -84,19 +85,20 @@ class TestCaseWithDB(TestCase):
                                               'id', item.id_):
                 db_found += [self.checked_class.from_table_row(self.db_conn,
                                                                row)]
                                               'id', item.id_):
                 db_found += [self.checked_class.from_table_row(self.db_conn,
                                                                row)]
-        self.assertEqual(sorted(content), sorted(db_found))
+        hashes_db_found = [hash(x) for x in db_found]
+        self.assertEqual(sorted(hashes_content), sorted(hashes_db_found))
 
     def check_saving_and_caching(self, **kwargs: Any) -> None:
         """Test instance.save in its core without relations."""
         obj = self.checked_class(**kwargs)  # pylint: disable=not-callable
         # check object init itself doesn't store anything yet
         self.check_storage([])
 
     def check_saving_and_caching(self, **kwargs: Any) -> None:
         """Test instance.save in its core without relations."""
         obj = self.checked_class(**kwargs)  # pylint: disable=not-callable
         # check object init itself doesn't store anything yet
         self.check_storage([])
-        # check saving stores in cache and DB
+        # check saving sets core attributes properly
         obj.save(self.db_conn)
         obj.save(self.db_conn)
-        self.check_storage([obj])
-        # check core attributes set properly (and not unset by saving)
         for key, value in kwargs.items():
             self.assertEqual(getattr(obj, key), value)
         for key, value in kwargs.items():
             self.assertEqual(getattr(obj, key), value)
+        # check saving stored properly in cache and DB
+        self.check_storage([obj])
 
     def check_saving_of_versioned(self, attr_name: str, type_: type) -> None:
         """Test owner's versioned attributes."""
 
     def check_saving_of_versioned(self, attr_name: str, type_: type) -> None:
         """Test owner's versioned attributes."""
@@ -106,7 +108,6 @@ class TestCaseWithDB(TestCase):
         attr.set(vals[0])
         attr.set(vals[1])
         owner.save(self.db_conn)
         attr.set(vals[0])
         attr.set(vals[1])
         owner.save(self.db_conn)
-        owner.uncache()
         retrieved = owner.__class__.by_id(self.db_conn, owner.id_)
         attr = getattr(retrieved, attr_name)
         self.assertEqual(sorted(attr.history.values()), vals)
         retrieved = owner.__class__.by_id(self.db_conn, owner.id_)
         attr = getattr(retrieved, attr_name)
         self.assertEqual(sorted(attr.history.values()), vals)
@@ -136,9 +137,11 @@ class TestCaseWithDB(TestCase):
         assert isinstance(obj.id_, type(self.default_ids[0]))
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', obj.id_):
         assert isinstance(obj.id_, type(self.default_ids[0]))
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', obj.id_):
+            hash_original = hash(obj)
             retrieved = self.checked_class.from_table_row(self.db_conn, row)
             retrieved = self.checked_class.from_table_row(self.db_conn, row)
-            self.assertEqual(obj, retrieved)
-            self.assertEqual({obj.id_: obj}, self.checked_class.get_cache())
+            self.assertEqual(hash_original, hash(retrieved))
+            self.assertEqual({retrieved.id_: retrieved},
+                             self.checked_class.get_cache())
 
     def check_versioned_from_table_row(self, attr_name: str,
                                        type_: type) -> None:
 
     def check_versioned_from_table_row(self, attr_name: str,
                                        type_: type) -> None: