X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=plomtask%2Fdb.py;fp=plomtask%2Fdb.py;h=99998a6ab29f760ba0d62f90395739dad4b521ff;hb=c5449a0b00f8865b1129ed56bdd16f1cc055bc87;hp=df98dd0f130bbd75553b2e628cd739d793e98616;hpb=b4a34a415fb31a00ee1e092fcc2a6b5d97edd52a;p=plomtask diff --git a/plomtask/db.py b/plomtask/db.py index df98dd0..99998a6 100644 --- a/plomtask/db.py +++ b/plomtask/db.py @@ -102,9 +102,7 @@ class DatabaseFile: @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.""" @@ -240,6 +238,7 @@ class BaseModel(Generic[BaseModelId]): 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: @@ -273,18 +272,26 @@ class BaseModel(Generic[BaseModelId]): 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: @@ -299,18 +306,40 @@ class BaseModel(Generic[BaseModelId]): 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.') - 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 - 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.') - cache = self.__class__.get_cache() + cache = self.get_cache() 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: - """Make from DB row, write to DB cache.""" + """Make from DB row, update DB cache with it.""" obj = cls(*row) - obj.cache() + obj._cache() 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) - obj.cache() 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] - 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: @@ -448,13 +476,12 @@ class BaseModel(Generic[BaseModelId]): 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_) - self.uncache() + self._uncache() db_conn.delete_where(self.table_name, 'id', self.id_) + self._disappear()