Update and add missing new docstrings

This commit is contained in:
miruka 2020-05-21 20:45:02 -04:00
parent cc1403974c
commit 8c9b5267e9
8 changed files with 85 additions and 5 deletions

View File

@ -1,5 +1,8 @@
# TODO # TODO
- pdb
- remove await_model_item
- uplaod/download use kwargs
- highlight messages being responded to - highlight messages being responded to
- highlight messages with mention - highlight messages with mention
- add room members loading indicator - add room members loading indicator
@ -9,7 +12,6 @@
it won't be visible in timeline no matter what the user config is it won't be visible in timeline no matter what the user config is
- fix: there are rooms without messages on first sync - fix: there are rooms without messages on first sync
- update docstrings
- update flatpak nio required version - update flatpak nio required version
- final test - final test

View File

@ -45,7 +45,7 @@ class Backend:
synchronized between the Python backend and the QML UI. synchronized between the Python backend and the QML UI.
The models should only ever be modified from the backend. 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. associated `Model` and returned.
The mapping keys are the `Model`'s synchronization ID, The mapping keys are the `Model`'s synchronization ID,
@ -66,6 +66,17 @@ class Backend:
- `("<user_id>", "<room_id>", "events")`: state events and messages - `("<user_id>", "<room_id>", "events")`: state events and messages
in the room `room_id` that our account `user_id` is part of. 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
- `("<user_id>", "<room_id>", "filtered_members")`:
See `models.special_models.FilteredMembers` docstring
clients: A `{user_id: MatrixClient}` dict for the logged-in clients clients: A `{user_id: MatrixClient}` dict for the logged-in clients
we managed. Every client is logged to one matrix account. 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: 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 if isinstance(model_id, list): # QML can't pass tuples
model_id = tuple(model_id) model_id = tuple(model_id)
@ -333,4 +349,8 @@ class Backend:
async def set_account_collapse(self, user_id: str, collapse: bool) -> None: 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) self.models["all_rooms"].set_account_collapse(user_id, collapse)

View File

@ -1242,7 +1242,7 @@ class MatrixClient(nio.AsyncClient):
left: bool = False, left: bool = False,
force_register_members: bool = False, force_register_members: bool = False,
) -> None: ) -> None:
"""Register a `nio.MatrixRoom` as a `Room` object in our model.""" """Register/update a `nio.MatrixRoom` as a `models.items.Room`."""
# Add room # Add room
inviter = getattr(room, "inviter", "") or "" 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: 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] member = room.users[user_id]
self.models[self.user_id, room.room_id, "members"][user_id] = Member( 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: 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) 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) 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, override_fetch_profile: Optional[bool] = None,
**fields, **fields,
) -> None: ) -> 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) await self.register_nio_room(room)

View File

@ -13,6 +13,8 @@ if TYPE_CHECKING:
class ModelFilter(ModelProxy): class ModelFilter(ModelProxy):
"""Filter data from one or more source models."""
def __init__(self, sync_id: SyncId) -> None: def __init__(self, sync_id: SyncId) -> None:
self.filtered_out: Dict[Tuple[Optional[SyncId], str], "ModelItem"] = {} self.filtered_out: Dict[Tuple[Optional[SyncId], str], "ModelItem"] = {}
self.items_changed_callbacks: List[Callable[[], None]] = [] self.items_changed_callbacks: List[Callable[[], None]] = []
@ -20,6 +22,7 @@ class ModelFilter(ModelProxy):
def accept_item(self, item: "ModelItem") -> bool: def accept_item(self, item: "ModelItem") -> bool:
"""Return whether an item should be present or filtered out."""
return True return True
@ -72,6 +75,8 @@ class ModelFilter(ModelProxy):
self, self,
only_if: Optional[Callable[["ModelItem"], bool]] = None, only_if: Optional[Callable[["ModelItem"], bool]] = None,
) -> None: ) -> None:
"""Recheck every item to decide if they should be filtered out."""
with self._write_lock: with self._write_lock:
take_out = [] take_out = []
bring_back = [] bring_back = []
@ -103,6 +108,17 @@ class ModelFilter(ModelProxy):
class FieldSubstringFilter(ModelFilter): 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: def __init__(self, sync_id: SyncId, fields: Collection[str]) -> None:
self.fields: Collection[str] = fields self.fields: Collection[str] = fields
self._filter: str = "" self._filter: str = ""

View File

@ -181,6 +181,12 @@ class Model(MutableMapping):
@contextmanager @contextmanager
def batch_remove(self): 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: try:
self._active_batch_remove_indice = [] self._active_batch_remove_indice = []
yield None yield None

View File

@ -22,7 +22,11 @@ class ModelStore(UserDict):
def __missing__(self, key: SyncId) -> Model: 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) is_tuple = isinstance(key, tuple)
@ -51,6 +55,8 @@ class ModelStore(UserDict):
async def ensure_exists_from_qml(self, sync_id: SyncId) -> None: 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 if isinstance(sync_id, list): # QML can't pass tuples
sync_id = tuple(sync_id) sync_id = tuple(sync_id)

View File

@ -10,6 +10,8 @@ if TYPE_CHECKING:
class ModelProxy(Model): class ModelProxy(Model):
"""Proxies data from one or more `Model` objects."""
def __init__(self, sync_id: SyncId) -> None: def __init__(self, sync_id: SyncId) -> None:
super().__init__(sync_id) super().__init__(sync_id)
self.take_items_ownership = False self.take_items_ownership = False
@ -22,10 +24,20 @@ class ModelProxy(Model):
def accept_source(self, source: Model) -> bool: def accept_source(self, source: Model) -> bool:
"""Return whether passed `Model` should be proxied by this proxy."""
return True return True
def convert_item(self, item: "ModelItem") -> "ModelItem": 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 return item
@ -36,17 +48,23 @@ class ModelProxy(Model):
value: "ModelItem", value: "ModelItem",
_changed_fields: Optional[Dict[str, Any]] = None, _changed_fields: Optional[Dict[str, Any]] = None,
) -> None: ) -> None:
"""Called when a source model item is added or changed."""
if self.accept_source(source): if self.accept_source(source):
value = self.convert_item(value) value = self.convert_item(value)
self.__setitem__((source.sync_id, key), value, _changed_fields) self.__setitem__((source.sync_id, key), value, _changed_fields)
def source_item_deleted(self, source: Model, key) -> None: def source_item_deleted(self, source: Model, key) -> None:
"""Called when a source model item is removed."""
if self.accept_source(source): if self.accept_source(source):
del self[source.sync_id, key] del self[source.sync_id, key]
def source_cleared(self, source: Model) -> None: def source_cleared(self, source: Model) -> None:
"""Called when a source model is cleared."""
if self.accept_source(source): if self.accept_source(source):
with self.batch_remove(): with self.batch_remove():
for source_sync_id, key in self.copy(): for source_sync_id, key in self.copy():

View File

@ -10,6 +10,8 @@ from .model_item import ModelItem
class AllRooms(FieldSubstringFilter): class AllRooms(FieldSubstringFilter):
"""Flat filtered list of all accounts and their rooms."""
def __init__(self, accounts: Model) -> None: def __init__(self, accounts: Model) -> None:
super().__init__(sync_id="all_rooms", fields=("display_name",)) super().__init__(sync_id="all_rooms", fields=("display_name",))
self.items_changed_callbacks.append(self.refilter_accounts) 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: 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): def only_if(item):
return item.type is Room and item.for_account == user_id return item.type is Room and item.for_account == user_id
@ -74,6 +78,10 @@ class AllRooms(FieldSubstringFilter):
class MatchingAccounts(ModelFilter): 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: def __init__(self, all_rooms: AllRooms) -> None:
self.all_rooms = all_rooms self.all_rooms = all_rooms
self.all_rooms.items_changed_callbacks.append(self.refilter) self.all_rooms.items_changed_callbacks.append(self.refilter)
@ -96,6 +104,8 @@ class MatchingAccounts(ModelFilter):
class FilteredMembers(FieldSubstringFilter): class FilteredMembers(FieldSubstringFilter):
"""Filtered list of members for a room."""
def __init__(self, user_id: str, room_id: str) -> None: def __init__(self, user_id: str, room_id: str) -> None:
self.user_id = user_id self.user_id = user_id
self.room_id = room_id self.room_id = room_id