From 77d877047b359c4334fd62dcc7c42ff5f7f72c36 Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 12 Mar 2020 14:41:00 -0400 Subject: [PATCH] Update docstrings --- TODO.md | 1 - src/backend/backend.py | 37 +++++++++++++++---------------- src/backend/matrix_client.py | 4 ++-- src/backend/media_cache.py | 30 ++++++++++++++++++++----- src/backend/models/model.py | 15 +++++-------- src/backend/models/model_item.py | 2 +- src/backend/models/model_store.py | 2 +- src/backend/pyotherside_events.py | 10 ++++++++- src/backend/utils.py | 26 ++++++++++++++++------ 9 files changed, 79 insertions(+), 48 deletions(-) diff --git a/TODO.md b/TODO.md index e3b72586..01e848b9 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ - Catch server 5xx errors when sending message and retry - nio ClientTimeout (to fix sync hanging after network changes or hibernation) -- Update docstrings - Update README.md ## Refactoring diff --git a/src/backend/backend.py b/src/backend/backend.py index c344f531..b6dfee84 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -29,43 +29,42 @@ class Backend: """Manage matrix clients and provide other useful general methods. Attributes: - saved_accounts: User config file for saved matrix account details. + saved_accounts: User config file for saved matrix account. ui_settings: User config file for QML interface settings. ui_state: User data file for saving/restoring QML UI state. - history: User data file for saving/restoring lines typed into QML + history: User data file for saving/restoring text typed into QML components. - models: A dict containing our data models that are synchronized between - the Python backend and the QML UI. + models: A mapping containing our data models that are + synchronized between the Python backend and the QML UI. The models should only ever be modified from the backend. - The dict keys are the `Model`'s synchronization ID, - which are in one of these form: + If a non-existent key is accessed, it is creating with an + associated `Model` and returned. - - A `ModelItem` type, the type of item that this model stores; - - A `(ModelItem, str...)` tuple. + The mapping keys are the `Model`'s synchronization ID, + which a strings or tuple of strings. Currently used sync ID throughout the code are: - - `Account`: logged-in accounts; + - `"accounts"`: logged-in accounts; - - `(Room, "")`: rooms a `user_id` account is part of; + - `("", "rooms")`: rooms our account `user_id` is part of; - - `(Upload, "")`: ongoing or failed file uploads for a - `user_id` account; + - `("", "uploads")`: ongoing or failed file uploads for + our account `user_id`; - - `(Member, "", "")`: members in the room - `room_id` that account `user_id` is part of; + - `("", "", "members")`: members in the room + `room_id` that our account `user_id` is part of; - - `(Event, "", "")`: state events and messages - in the room `room_id` that account `user_id` is part of. + - `("", "", "events")`: state events and messages + in the room `room_id` that our account `user_id` is part of. - clients: A dict containing the logged `MatrixClient` objects we manage. - Each client represents a logged-in account, - identified by its `user_id`. + clients: A `{user_id: MatrixClient}` dict for the logged-in clients + we managed. Every client is logged to one matrix account. media_cache: A matrix media cache for downloaded files. """ diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index dc742448..8b5a9aa0 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -357,7 +357,7 @@ class MatrixClient(nio.AsyncClient): async def _send_file( self, item_uuid: UUID, room_id: str, path: Union[Path, str], ) -> None: - """Monitorably upload a file + thumbnail and send the built event.""" + """Upload and monitor a file + thumbnail and send the built event.""" # TODO: this function is way too complex, and most of it should be # refactored into nio. @@ -993,7 +993,7 @@ class MatrixClient(nio.AsyncClient): async def clear_events(self, room_id: str) -> None: - """Remove every `Event` of a room we registred in our model. + """Remove every `Event` of a room we registered in our model. The events will be gone from the UI, until the client is restarted. """ diff --git a/src/backend/media_cache.py b/src/backend/media_cache.py index cba15be8..57b7992e 100644 --- a/src/backend/media_cache.py +++ b/src/backend/media_cache.py @@ -94,7 +94,15 @@ class Media: @property def local_path(self) -> Path: - """The path where the file either exists or should be downloaded.""" + """The path where the file either exists or should be downloaded. + + The returned paths are in this form: + ``` + // + _.` + ``` + e.g. `~/.cache/mirage/downloads/matrix.org/foo_Hm24ar11i768b0el.png`. + """ parsed = urlparse(self.mxc) mxc_id = parsed.path.lstrip("/") @@ -255,8 +263,12 @@ class Thumbnail(Media): """The path where the thumbnail either exists or should be downloaded. The returned paths are in this form: - `///`, - e.g. `~/.cache/appname/thumbnails/matrix.org/32x32/Hm24ar11i768b0el`. + ``` + /// + _.` + ``` + e.g. + `~/.cache/mirage/thumbnails/matrix.org/32x32/foo_Hm24ar11i768b0el.png`. """ size = self.normalize_size(self.server_size or self.wanted_size) @@ -271,12 +283,16 @@ class Thumbnail(Media): async def _get_local_existing_file(self) -> Path: + """Return an existing thumbnail path or raise `FileNotFoundError`. + + If we have a bigger size thumbnail downloaded than the `wanted_size` + for the media, return it instead of asking the server for a + smaller thumbnail. + """ + if self.local_path.exists(): return self.local_path - # If we have a bigger size thumbnail than the wanted_size for this pic, - # return it instead of asking the server for a smaller thumbnail. - try_sizes = ((32, 32), (96, 96), (320, 240), (640, 480), (800, 600)) parts = list(self.local_path.parts) size = self.normalize_size(self.server_size or self.wanted_size) @@ -295,6 +311,8 @@ class Thumbnail(Media): async def _get_remote_data(self) -> bytes: + """Return the (decrypted) media file's content from the server.""" + parsed = urlparse(self.mxc) if self.crypt_dict: diff --git a/src/backend/models/model.py b/src/backend/models/model.py index 9e1e01b6..947dd902 100644 --- a/src/backend/models/model.py +++ b/src/backend/models/model.py @@ -16,22 +16,17 @@ if TYPE_CHECKING: class Model(MutableMapping): - """A mapping of `{identifier: ModelItem}` synced between Python & QML. + """A mapping of `{ModelItem.id: ModelItem}` synced between Python & QML. From the Python side, the model is usable like a normal dict of `ModelItem` subclass objects. Different types of `ModelItem` must not be mixed in the same model. - When items are added, changed or removed from the model, a synchronization - with QML is scheduled. - The model will synchronize with QML no more than every 0.25s, for - performance reasons; though it is possible to request an instant sync - via `sync_now()` for certain cases when this delay is unacceptable. + When items are added, replaced, removed, have field value changes, or the + model is cleared, corresponding `PyOtherSideEvent` are fired to inform + QML of the changes so that it can keep its models in sync. - Model data is sent to QML using a `ModelUpdated` event from the - `pyotherside_events` module. - The data is a list of serialized `ModelItem` dicts, as expected - by QML for components like `ListView`. + Items in the model are kept sorted using the `ModelItem` subclass `__lt__`. """ def __init__(self, sync_id: SyncId) -> None: diff --git a/src/backend/models/model_item.py b/src/backend/models/model_item.py index fdc860c9..0177ed73 100644 --- a/src/backend/models/model_item.py +++ b/src/backend/models/model_item.py @@ -17,7 +17,7 @@ class ModelItem: Subclasses are also expected to implement `__lt__()`, to provide support for comparisons with the `<`, `>`, `<=`, `=>` operators - and thus allow a `Model` to sort its `ModelItem`s. + and thus allow a `Model` to keep its data sorted. """ def __new__(cls, *_args, **_kwargs) -> "ModelItem": diff --git a/src/backend/models/model_store.py b/src/backend/models/model_store.py index 7446313e..9fd1d881 100644 --- a/src/backend/models/model_store.py +++ b/src/backend/models/model_store.py @@ -14,7 +14,7 @@ class ModelStore(UserDict): The dict keys must be the sync ID of `Model` values. If a non-existent key is accessed, a corresponding `Model` will be - created, put into the interal `data` dict and returned. + created, put into the internal `data` dict and returned. """ data: Dict[SyncId, Model] = field(default_factory=dict) diff --git a/src/backend/pyotherside_events.py b/src/backend/pyotherside_events.py index ebc7e9fa..a54db7ba 100644 --- a/src/backend/pyotherside_events.py +++ b/src/backend/pyotherside_events.py @@ -55,7 +55,7 @@ class CoroutineDone(PyOtherSideEvent): @dataclass class LoopException(PyOtherSideEvent): - """Indicate that an uncaught exception occured in the asyncio loop.""" + """Indicate an uncaught exception occurance in the asyncio loop.""" message: str = field() exception: Optional[Exception] = field() @@ -64,6 +64,8 @@ class LoopException(PyOtherSideEvent): @dataclass class ModelItemInserted(PyOtherSideEvent): + """Indicate a `ModelItem` insertion into a `Backend` `Model`.""" + sync_id: SyncId = field() index: int = field() item: "ModelItem" = field() @@ -71,6 +73,8 @@ class ModelItemInserted(PyOtherSideEvent): @dataclass class ModelItemFieldChanged(PyOtherSideEvent): + """Indicate a `ModelItem`'s field value change in a `Backend` `Model`.""" + sync_id: SyncId = field() item_index_then: int = field() item_index_now: int = field() @@ -80,10 +84,14 @@ class ModelItemFieldChanged(PyOtherSideEvent): @dataclass class ModelItemDeleted(PyOtherSideEvent): + """Indicate the removal of a `ModelItem` from a `Backend` `Model`.""" + sync_id: SyncId = field() index: int = field() @dataclass class ModelCleared(PyOtherSideEvent): + """Indicate that a `Backend` `Model` was cleared.""" + sync_id: SyncId = field() diff --git a/src/backend/utils.py b/src/backend/utils.py index 29cd0546..e5401587 100644 --- a/src/backend/utils.py +++ b/src/backend/utils.py @@ -26,7 +26,7 @@ auto = autostr class AutoStrEnum(Enum): - """An Enum where auto() assigns the member's name instead of an int. + """An Enum where auto() assigns the member's name instead of an integer. Example: >>> class Fruits(AutoStrEnum): apple = auto() @@ -130,12 +130,24 @@ def serialize_value_for_qml(value: Any, json_list_dicts: bool = False) -> Any: Returns: - - Return the member's actual value for `Enum` members - - A `file://...` string for `Path` objects - - Strings for `UUID` objects - - A number of milliseconds for `datetime.timedelta` objects - - The class `__name__` for class types. - - `ModelItem.serialized` for `ModelItem`s + - For `int`, `float`, `bool`, `str` and `datetime`: the unchanged value + + - For `Sequence` and `Mapping` subclasses (includes `list` and `dict`): + a JSON dump if `json_list_dicts` is `True`, else the unchanged value + + - If the value as a `serialized` attribute or property, return that + + - For `Enum` members, the actual value of the member + + - For `Path` objects, a `file://` string + + - For `UUID` object: the UUID in string form + + - For `timedelta` objects: the delta as a number of milliseconds `int` + + - For class types: the class `__name__` + + - For anything else: the unchanged value """ if isinstance(value, (int, float, bool, str, datetime)):