From 3ea8eb81f60f8202f246bc2e1ebef6c34d76b27e Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:47:32 -0600 Subject: [PATCH] Model reprs read only loaded state (#819) A repr runs inside log calls. Reading a relationship or an expired column there runs a query, and on an object whose session is gone it raises DetachedInstanceError instead of logging. Archive's repr also stat'ed its SingleFile file on disk. `wrolpi.common.loaded()` follows attributes through state that is already loaded (primary keys from the identity key) and never runs SQL. Every model repr uses it; with everything loaded the output is unchanged, except Video and Archive now show the FileGroup's primary path. Co-authored-by: Claude Opus 5.5 --- modules/archive/models.py | 14 +++---- modules/docs/models.py | 9 +++-- modules/repos/models.py | 5 ++- modules/videos/models.py | 14 +++---- modules/zim/models.py | 25 ++++++------ wrolpi/collections/models.py | 9 +++-- wrolpi/common.py | 25 ++++++++++++ wrolpi/downloader.py | 15 +++---- wrolpi/files/models.py | 9 +++-- wrolpi/tags.py | 10 +++-- wrolpi/test/test_model_repr.py | 71 ++++++++++++++++++++++++++++++++++ 11 files changed, 154 insertions(+), 52 deletions(-) create mode 100644 wrolpi/test/test_model_repr.py diff --git a/modules/archive/models.py b/modules/archive/models.py index 0931f1a6..4296dea6 100644 --- a/modules/archive/models.py +++ b/modules/archive/models.py @@ -9,8 +9,8 @@ from sqlalchemy.orm import Mapped, relationship, Session, joinedload from wrolpi import dates from wrolpi.collections import Collection -from wrolpi.common import ModelHelper, Base, logger, get_title_from_html, get_wrolpi_config, get_media_directory, \ - strip_surrogates +from wrolpi.common import loaded, ModelHelper, Base, logger, get_title_from_html, get_wrolpi_config, \ + get_media_directory, strip_surrogates from wrolpi.dates import now from wrolpi.db import serializer from wrolpi.errors import UnknownArchive @@ -60,11 +60,11 @@ class Archive(Base, ModelHelper): return d def __repr__(self): - domain_name = self.domain if self.collection else None - if domain_name: - return f'' - return f'' + id_, url = loaded(self, 'id'), loaded(self, 'file_group', 'url') + path = repr(str(loaded(self, 'file_group', 'primary_path'))) + if domain_name := loaded(self, 'collection', 'name'): + return f'' + return f'' @property def domain(self) -> str | None: diff --git a/modules/docs/models.py b/modules/docs/models.py index 84cdc562..793782fc 100644 --- a/modules/docs/models.py +++ b/modules/docs/models.py @@ -4,7 +4,7 @@ from typing import Optional from sqlalchemy import Column, Integer, BigInteger, ForeignKey, String, Text, Index, or_ from sqlalchemy.orm import Mapped, relationship, Session, joinedload -from wrolpi.common import ModelHelper, Base +from wrolpi.common import loaded, ModelHelper, Base from wrolpi.db import get_db_session, serializer from wrolpi.files.models import FileGroup @@ -62,8 +62,8 @@ class Doc(ModelHelper, Base): passive_deletes=True) def __repr__(self): - path = str(self.file_group.primary_path) if self.file_group else 'None' - return f'' + path = str(loaded(self, 'file_group', 'primary_path')) + return f'' @staticmethod def json_options() -> tuple: @@ -166,4 +166,5 @@ class DocSection(ModelHelper, Base): doc = relationship('Doc', back_populates='sections') def __repr__(self): - return f'' + return f'' diff --git a/modules/repos/models.py b/modules/repos/models.py index 5022f8dc..3c3c9ad7 100644 --- a/modules/repos/models.py +++ b/modules/repos/models.py @@ -5,7 +5,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, BigInteger, Text, Bo from sqlalchemy.orm import relationship, Session, joinedload, selectinload, deferred from wrolpi.collections.models import Collection -from wrolpi.common import Base, ModelHelper, get_relative_to_media_directory +from wrolpi.common import loaded, Base, ModelHelper, get_relative_to_media_directory from wrolpi.dates import TZDateTime from wrolpi.db import serializer from wrolpi.downloader import Download @@ -57,7 +57,8 @@ class Repository(ModelHelper, Base): uselist=False) def __repr__(self): - return f'' + return f'' @property def name(self) -> Optional[str]: diff --git a/modules/videos/models.py b/modules/videos/models.py index 60f31c61..3637cec8 100644 --- a/modules/videos/models.py +++ b/modules/videos/models.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, relationship, Session, deferred, undefer, joi from modules.videos.errors import UnknownVideo, UnknownChannel from wrolpi.captions import read_captions, read_captions_with_timestamps -from wrolpi.common import Base, ModelHelper, logger, get_media_directory, get_relative_to_media_directory, \ +from wrolpi.common import loaded, Base, ModelHelper, logger, get_media_directory, get_relative_to_media_directory, \ background_task from wrolpi.db import get_db_curs, get_db_session, session_curs, serializer from wrolpi.collections.models import Collection @@ -53,11 +53,10 @@ class Video(ModelHelper, Base): file_group: Mapped[FileGroup] = relationship('FileGroup') def __repr__(self): - v = None - if self.video_path: - v = repr(str(self.video_path.relative_to(get_media_directory()))) - return f'