From 8c9b5267e9fbedee265c1683fc87a4f1dc579648 Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 21 May 2020 20:45:02 -0400 Subject: [PATCH] Update and add missing new docstrings --- TODO.md | 4 +++- src/backend/backend.py | 22 +++++++++++++++++++++- src/backend/matrix_client.py | 6 ++++-- src/backend/models/filters.py | 16 ++++++++++++++++ src/backend/models/model.py | 6 ++++++ src/backend/models/model_store.py | 8 +++++++- src/backend/models/proxy.py | 18 ++++++++++++++++++ src/backend/models/special_models.py | 10 ++++++++++ 8 files changed, 85 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 20bda35d..e0b529e7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,8 @@ # TODO +- pdb +- remove await_model_item +- uplaod/download use kwargs - highlight messages being responded to - highlight messages with mention - add room members loading indicator @@ -9,7 +12,6 @@ it won't be visible in timeline no matter what the user config is - fix: there are rooms without messages on first sync -- update docstrings - update flatpak nio required version - final test diff --git a/src/backend/backend.py b/src/backend/backend.py index fd81c71d..beb2fc12 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -45,7 +45,7 @@ class Backend: synchronized between the Python backend and the QML UI. The models should only ever be modified from the backend. - If a non-existent key is accessed, it is creating with an + If a non-existent key is accessed, it is created and an associated `Model` and returned. The mapping keys are the `Model`'s synchronization ID, @@ -66,6 +66,17 @@ class Backend: - `("", "", "events")`: state events and messages in the room `room_id` that our account `user_id` is part of. + Special models: + + - `"all_rooms"`: See `models.special_models.AllRooms` docstring + + - `"matching_accounts"` + See `models.special_models.MatchingAccounts` docstring + + - `("", "", "filtered_members")`: + See `models.special_models.FilteredMembers` docstring + + clients: A `{user_id: MatrixClient}` dict for the logged-in clients we managed. Every client is logged to one matrix account. @@ -321,6 +332,11 @@ class Backend: async def set_substring_filter(self, model_id: SyncId, value: str) -> None: + """Set a FieldSubstringFilter model's filter property. + + This should only be called from QML. + """ + if isinstance(model_id, list): # QML can't pass tuples model_id = tuple(model_id) @@ -333,4 +349,8 @@ class Backend: async def set_account_collapse(self, user_id: str, collapse: bool) -> None: + """Call `set_account_collapse()` on the `all_rooms` model. + + This should only be called from QML. + """ self.models["all_rooms"].set_account_collapse(user_id, collapse) diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index d468d395..7c314cf8 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -1242,7 +1242,7 @@ class MatrixClient(nio.AsyncClient): left: bool = False, force_register_members: bool = False, ) -> None: - """Register a `nio.MatrixRoom` as a `Room` object in our model.""" + """Register/update a `nio.MatrixRoom` as a `models.items.Room`.""" # Add room inviter = getattr(room, "inviter", "") or "" @@ -1310,6 +1310,7 @@ class MatrixClient(nio.AsyncClient): async def add_member(self, room: nio.MatrixRoom, user_id: str) -> None: + """Register/update a room member into our models.""" member = room.users[user_id] self.models[self.user_id, room.room_id, "members"][user_id] = Member( @@ -1328,6 +1329,7 @@ class MatrixClient(nio.AsyncClient): async def remove_member(self, room: nio.MatrixRoom, user_id: str) -> None: + """Remove a room member from our models.""" self.models[self.user_id, room.room_id, "members"].pop(user_id, None) HTML.rooms_user_id_names[room.room_id].pop(user_id, None) @@ -1403,7 +1405,7 @@ class MatrixClient(nio.AsyncClient): override_fetch_profile: Optional[bool] = None, **fields, ) -> None: - """Register a `nio.Event` as a `Event` object in our model.""" + """Register/update a `nio.Event` as a `models.items.Event` object.""" await self.register_nio_room(room) diff --git a/src/backend/models/filters.py b/src/backend/models/filters.py index d10e7bcf..eebfabae 100644 --- a/src/backend/models/filters.py +++ b/src/backend/models/filters.py @@ -13,6 +13,8 @@ if TYPE_CHECKING: class ModelFilter(ModelProxy): + """Filter data from one or more source models.""" + def __init__(self, sync_id: SyncId) -> None: self.filtered_out: Dict[Tuple[Optional[SyncId], str], "ModelItem"] = {} self.items_changed_callbacks: List[Callable[[], None]] = [] @@ -20,6 +22,7 @@ class ModelFilter(ModelProxy): def accept_item(self, item: "ModelItem") -> bool: + """Return whether an item should be present or filtered out.""" return True @@ -72,6 +75,8 @@ class ModelFilter(ModelProxy): self, only_if: Optional[Callable[["ModelItem"], bool]] = None, ) -> None: + """Recheck every item to decide if they should be filtered out.""" + with self._write_lock: take_out = [] bring_back = [] @@ -103,6 +108,17 @@ class ModelFilter(ModelProxy): class FieldSubstringFilter(ModelFilter): + """Filter source models based on if their fields matches a string. + + This is used for filter fields in QML: the user enters some text and only + items with a certain field (typically `display_name`) that contain the + words of the text (can be partial, e.g. "red" matches "red" or "tired") + will be shown. + + Matching is done using "smart case": insensitive if the filter text is + all lowercase, sensitive otherwise. + """ + def __init__(self, sync_id: SyncId, fields: Collection[str]) -> None: self.fields: Collection[str] = fields self._filter: str = "" diff --git a/src/backend/models/model.py b/src/backend/models/model.py index 8ebd9ac6..e1422789 100644 --- a/src/backend/models/model.py +++ b/src/backend/models/model.py @@ -181,6 +181,12 @@ class Model(MutableMapping): @contextmanager def batch_remove(self): + """Context manager that accumulates item removal events. + + When the context manager exits, sequences of removed items are grouped + and one `ModelItemDeleted` pyotherside event is fired per sequence. + """ + try: self._active_batch_remove_indice = [] yield None diff --git a/src/backend/models/model_store.py b/src/backend/models/model_store.py index 292e06bc..a65aa60e 100644 --- a/src/backend/models/model_store.py +++ b/src/backend/models/model_store.py @@ -22,7 +22,11 @@ class ModelStore(UserDict): def __missing__(self, key: SyncId) -> Model: - """When accessing a non-existent model, create and return it.""" + """When accessing a non-existent model, create and return it. + + Special models rather than a generic `Model` object may be returned + depending on the passed key. + """ is_tuple = isinstance(key, tuple) @@ -51,6 +55,8 @@ class ModelStore(UserDict): async def ensure_exists_from_qml(self, sync_id: SyncId) -> None: + """Create model if it doesn't exist. Should only be called by QML.""" + if isinstance(sync_id, list): # QML can't pass tuples sync_id = tuple(sync_id) diff --git a/src/backend/models/proxy.py b/src/backend/models/proxy.py index f61e2a2c..8bbfab9a 100644 --- a/src/backend/models/proxy.py +++ b/src/backend/models/proxy.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: class ModelProxy(Model): + """Proxies data from one or more `Model` objects.""" + def __init__(self, sync_id: SyncId) -> None: super().__init__(sync_id) self.take_items_ownership = False @@ -22,10 +24,20 @@ class ModelProxy(Model): def accept_source(self, source: Model) -> bool: + """Return whether passed `Model` should be proxied by this proxy.""" return True def convert_item(self, item: "ModelItem") -> "ModelItem": + """Take a source `ModelItem`, return an appropriate one for proxy. + + By default, this returns the passed item unchanged. + + Due to QML `ListModel` restrictions, if multiple source models + containing different subclasses of `ModelItem` are proxied, + they should be converted to a same `ModelItem` + subclass by overriding this function. + """ return item @@ -36,17 +48,23 @@ class ModelProxy(Model): value: "ModelItem", _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: + """Called when a source model item is added or changed.""" + if self.accept_source(source): value = self.convert_item(value) self.__setitem__((source.sync_id, key), value, _changed_fields) def source_item_deleted(self, source: Model, key) -> None: + """Called when a source model item is removed.""" + if self.accept_source(source): del self[source.sync_id, key] def source_cleared(self, source: Model) -> None: + """Called when a source model is cleared.""" + if self.accept_source(source): with self.batch_remove(): for source_sync_id, key in self.copy(): diff --git a/src/backend/models/special_models.py b/src/backend/models/special_models.py index f18f3c56..582582cb 100644 --- a/src/backend/models/special_models.py +++ b/src/backend/models/special_models.py @@ -10,6 +10,8 @@ from .model_item import ModelItem class AllRooms(FieldSubstringFilter): + """Flat filtered list of all accounts and their rooms.""" + def __init__(self, accounts: Model) -> None: super().__init__(sync_id="all_rooms", fields=("display_name",)) self.items_changed_callbacks.append(self.refilter_accounts) @@ -20,6 +22,8 @@ class AllRooms(FieldSubstringFilter): def set_account_collapse(self, user_id: str, collapsed: bool) -> None: + """Set whether the rooms for an account should be filtered out.""" + def only_if(item): return item.type is Room and item.for_account == user_id @@ -74,6 +78,10 @@ class AllRooms(FieldSubstringFilter): class MatchingAccounts(ModelFilter): + """List of our accounts in `AllRooms` with at least one matching room if + a `filter` is set, else list of all accounts. + """ + def __init__(self, all_rooms: AllRooms) -> None: self.all_rooms = all_rooms self.all_rooms.items_changed_callbacks.append(self.refilter) @@ -96,6 +104,8 @@ class MatchingAccounts(ModelFilter): class FilteredMembers(FieldSubstringFilter): + """Filtered list of members for a room.""" + def __init__(self, user_id: str, room_id: str) -> None: self.user_id = user_id self.room_id = room_id