From 61cd3b2f55890ff1a8a07d203e20e34ad3267035 Mon Sep 17 00:00:00 2001 From: miruka Date: Wed, 18 Dec 2019 08:41:02 -0400 Subject: [PATCH] =?UTF-8?q?Rename=20config=5Ffiles=20module=20=E2=86=92=20?= =?UTF-8?q?user=5Ffiles=20+=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/backend.py | 12 +-- .../{config_files.py => user_files.py} | 79 ++++++++++++++----- 2 files changed, 66 insertions(+), 25 deletions(-) rename src/backend/{config_files.py => user_files.py} (75%) diff --git a/src/backend/backend.py b/src/backend/backend.py index afec514f..e6c7efaa 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -28,11 +28,11 @@ class Backend: def __init__(self) -> None: self.appdirs = AppDirs(appname=__app_name__, roaming=True) - from . import config_files - self.saved_accounts = config_files.Accounts(self) - self.ui_settings = config_files.UISettings(self) - self.ui_state = config_files.UIState(self) - self.history = config_files.History(self) + from . import user_files + self.saved_accounts = user_files.Accounts(self) + self.ui_settings = user_files.UISettings(self) + self.ui_state = user_files.UIState(self) + self.history = user_files.History(self) self.models = ModelStore(allowed_key_types={ Account, # Logged-in accounts @@ -232,7 +232,7 @@ class Backend: async def load_settings(self) -> tuple: """Return parsed user config files.""" - from .config_files import Theme + from .user_files import Theme settings = await self.ui_settings.read() ui_state = await self.ui_state.read() history = await self.history.read() diff --git a/src/backend/config_files.py b/src/backend/user_files.py similarity index 75% rename from src/backend/config_files.py rename to src/backend/user_files.py index 19e9e33f..9c80d198 100644 --- a/src/backend/config_files.py +++ b/src/backend/user_files.py @@ -1,9 +1,11 @@ +"""User data and configuration files definitions.""" + import asyncio import json import logging as log from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, ClassVar, Dict, Optional import aiofiles @@ -17,7 +19,11 @@ WRITE_LOCK = asyncio.Lock() @dataclass -class ConfigFile: +class DataFile: + """Base class representing a user data file.""" + + is_config: ClassVar[bool] = False + backend: Backend = field(repr=False) filename: str = field() @@ -30,23 +36,36 @@ class ConfigFile: @property def path(self) -> Path: - return Path(self.backend.appdirs.user_config_dir) / self.filename + """Full path of the file, even if it doesn't exist yet.""" + + if self.is_config: + return Path(self.backend.appdirs.user_config_dir) / self.filename + + return Path(self.backend.appdirs.user_data_dir) / self.filename async def default_data(self): + """Default content if the file doesn't exist.""" + return "" async def read(self): + """Return the content of the existing file on disk.""" + log.debug("Reading config %s at %s", type(self).__name__, self.path) return self.path.read_text() async def write(self, data) -> None: + """Request for the file to be written/updated with data.""" + self._to_write = data async def _write_loop(self) -> None: + """Write/update file to on disk with a 1 second cooldown.""" + self.path.parent.mkdir(parents=True, exist_ok=True) while True: @@ -59,13 +78,21 @@ class ConfigFile: await asyncio.sleep(1) + @dataclass -class JSONConfigFile(ConfigFile): +class JSONDataFile(DataFile): + """Represent a user data file in the JSON format.""" + async def default_data(self) -> JsonData: return {} async def read(self) -> JsonData: + """Return the content of the existing file on disk. + + If the file doesn't exist on disk or it has missing keys, the missing + data will be merged and written to disk before returning. + """ try: data = json.loads(await super().read()) except (FileNotFoundError, json.JSONDecodeError): @@ -86,15 +113,26 @@ class JSONConfigFile(ConfigFile): @dataclass -class Accounts(JSONConfigFile): +class Accounts(JSONDataFile): + """Config file for saved matrix accounts: user ID, access tokens, etc.""" + + is_config = True + filename: str = "accounts.json" async def any_saved(self) -> bool: + """Return whether there are any accounts saved on disk.""" return bool(await self.read()) async def add(self, user_id: str) -> None: + """Add an account to the config and write it on disk. + + The account's details such as its access token are retrieved from + the corresponding `MatrixClient` in `backend.clients`. + """ + client = self.backend.clients[user_id] await self.write({ @@ -108,6 +146,8 @@ class Accounts(JSONConfigFile): async def delete(self, user_id: str) -> None: + """Delete an account from the config and write it on disk.""" + await self.write({ uid: info for uid, info in (await self.read()).items() if uid != user_id @@ -115,7 +155,11 @@ class Accounts(JSONConfigFile): @dataclass -class UISettings(JSONConfigFile): +class UISettings(JSONDataFile): + """Config file for QML interface settings and keybindings.""" + + is_config = True + filename: str = "settings.json" @@ -174,15 +218,12 @@ class UISettings(JSONConfigFile): @dataclass -class UIState(JSONConfigFile): +class UIState(JSONDataFile): + """File to save and restore the state of the QML interface.""" + filename: str = "state.json" - @property - def path(self) -> Path: - return Path(self.backend.appdirs.user_data_dir) / self.filename - - async def default_data(self) -> JsonData: return { "collapseAccounts": {}, @@ -192,21 +233,21 @@ class UIState(JSONConfigFile): @dataclass -class History(JSONConfigFile): +class History(JSONDataFile): + """File to save and restore lines typed by the user in QML components.""" + filename: str = "history.json" - @property - def path(self) -> Path: - return Path(self.backend.appdirs.user_data_dir) / self.filename - - async def default_data(self) -> JsonData: return {"console": []} @dataclass -class Theme(ConfigFile): +class Theme(DataFile): + """A theme file defining the look of QML components.""" + + @property def path(self) -> Path: data_dir = Path(self.backend.appdirs.user_data_dir)