From 14c6db3384ea31a37dff58788d660b98e75a93c7 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 10 May 2026 16:14:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(config):=20=E5=BC=95=E5=85=A5=20Config?= =?UTF-8?q?Path=20=E5=80=BC=E5=AF=B9=E8=B1=A1=E6=B6=88=E9=99=A4=20ConfigTy?= =?UTF-8?q?pe/ConfigKey=20=E7=9A=84=E6=B6=88=E8=B4=B9=E8=80=85=20API=20?= =?UTF-8?q?=E5=86=97=E4=BD=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALConfigWidget.py | 11 +-- src/gui/ALTimerTaskManageWidget.py | 10 ++- src/interfaces/ConfigProvider.py | 117 +++++++++++++++++++++++++++ src/interfaces/__init__.py | 11 +++ src/managers/config/ConfigManager.py | 33 +++----- src/managers/config/ConfigUtils.py | 6 +- 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/interfaces/ConfigProvider.py create mode 100644 src/interfaces/__init__.py diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index d97d9b5..052b580 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -24,6 +24,7 @@ import managers.config.ConfigManager as ConfigManager from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter +from interfaces.ConfigProvider import ConfigProvider, CfgKey from managers.config.ConfigUtils import ConfigUtils from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget @@ -43,7 +44,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ): super().__init__(parent) - self.__cfg_mgr = ConfigManager.instance() + self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__config_data = {"run": {}, "user": {}} @@ -985,13 +986,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.setRunConfigToWidget(data) self.__config_paths["run"] = run_config_path self.CurrentRunConfigEdit.setText(run_config_path) - paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", []) + paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, []) if run_config_path not in paths: paths.append(run_config_path) index = len(paths) - 1 else: index = paths.index(run_config_path) - self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths}) + self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.ROOT, {"current": index, "paths": paths}) else: QMessageBox.warning( self, @@ -1020,13 +1021,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.setUsersToTreeWidget(data) self.__config_paths["user"] = user_config_path self.CurrentUserConfigEdit.setText(user_config_path) - paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", []) + paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.USER_PATH.PATHS, []) if user_config_path not in paths: paths.append(user_config_path) index = len(paths) - 1 else: index = paths.index(user_config_path) - self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths}) + self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.USER_PATH.ROOT, {"current": index, "paths": paths}) else: QMessageBox.warning( self, diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index c603903..4aef755 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -26,7 +26,9 @@ from PySide6.QtGui import ( ) import managers.config.ConfigManager as ConfigManager + from utils.TimerUtils import TimerUtils +from interfaces.ConfigProvider import ConfigProvider, CfgKey from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus @@ -190,7 +192,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): super().__init__(parent) - self.__cfg_mgr = ConfigManager.instance() + self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__timer_tasks = [] self.__check_timer = None self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME @@ -244,7 +246,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ) -> list: try: - timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK) + timer_tasks = self.__cfg_mgr.get(CfgKey.TIMERTASK.ROOT) if timer_tasks and "timer_tasks" in timer_tasks: for task in timer_tasks["timer_tasks"]: task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S") @@ -277,7 +279,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): if "repeat_history" in task: for item in task["repeat_history"]: item["result"] = item["result"].value - self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks }) + self.__cfg_mgr.set(CfgKey.TIMERTASK.ROOT, { "timer_tasks": timer_tasks }) return True except Exception as e: QMessageBox.warning( @@ -437,7 +439,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n" f"已记录次数:{history_count}" ) - + def deleteTask( self, diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py new file mode 100644 index 0000000..bf5ebb3 --- /dev/null +++ b/src/interfaces/ConfigProvider.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional, Protocol + + +class ConfigType(Enum): + """ + Config type enum. Values represent the default filename. + """ + GLOBAL = "autolibrary.json" + BULLETIN = "bulletin.json" + TIMERTASK = "timer_task.json" + + +@dataclass(frozen=True) +class ConfigPath: + """ + A typed configuration path that carries both the config file + and the dot-separated key in a single object. + + Consumers pass this directly to ConfigProvider.get/set, + eliminating the need to import ConfigType separately. + """ + config_type: ConfigType + key: str = "" + + +class CfgKey: + """ + Type-safe hierarchical configuration key constants. + + Each leaf is a ConfigPath that can be passed directly to + ``ConfigProvider.get()`` or ``ConfigProvider.set()``. + + Usage:: + + CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS + # -> ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths") + + config.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, []) + config.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, value) + """ + + class GLOBAL: + class AUTOMATION: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation") + + class RUN_PATH: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation.run_path") + CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.run_path.current") + PATHS = ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths") + + class USER_PATH: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation.user_path") + CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current") + PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths") + + class TIMERTASK: + ROOT = ConfigPath(ConfigType.TIMERTASK, "") + TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks") + + class BULLETIN: + ROOT = ConfigPath(ConfigType.BULLETIN, "") + BULLETIN = ConfigPath(ConfigType.BULLETIN, "bulletin") + LAST_SYNC_TIME = ConfigPath(ConfigType.BULLETIN, "last_sync_time") + + +class ConfigProvider(Protocol): + """ + Abstract interface for configuration storage access. + + Concrete implementations (e.g. ConfigManager) conform to + this protocol structurally rather than through explicit + inheritance. + """ + + def get( + self, + key: ConfigPath, + default: Optional[Any] = None + ) -> Any: + """ + Retrieve a configuration value. + + Args: + key: A ConfigPath object specifying which config file + and key to read from. + default: Fallback value if the key is not found. + + Returns: + The configuration value at the given key path. + """ + ... + + def set( + self, + key: ConfigPath, + value: Any = None + ) -> None: + """ + Set a configuration value and persist to disk. + + Args: + key: A ConfigPath object specifying which config file + and key to write to. + value: The value to store. + """ + ... diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py new file mode 100644 index 0000000..2df5c1b --- /dev/null +++ b/src/interfaces/__init__.py @@ -0,0 +1,11 @@ +""" + Interfaces module for the AutoLibrary project. + + Defines abstract interfaces (Protocols) and shared type definitions + used across layers to decouple consumers from concrete implementations. + + Key components: + - ConfigProvider: Abstract interface for configuration access. + - ConfigType: Enumeration of configuration file types. + - ConfigKey: Type-safe hierarchical key constants for config lookups. +""" diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index b415442..1dc108f 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -10,26 +10,17 @@ See the LICENSE file for details. import os import threading -from enum import Enum from typing import Any, Optional from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter +from interfaces.ConfigProvider import ConfigType, ConfigPath # This config manager class only responsible for global and other # unconfigurable config files. -class ConfigType(Enum): - """ - Config type class. Values represent the default filename. - """ - GLOBAL = "autolibrary.json" # Global config file. - BULLETIN = "bulletin.json" # Bulletin board config file. - TIMERTASK = "timer_task.json" # Timer task config file. - - class ConfigTemplate: """ Config template class. @@ -120,16 +111,15 @@ class ConfigManager: def get( self, - config_type: ConfigType, - key: str = "", + key: ConfigPath, default: Optional[Any] = None ) -> Any: with self.__config_lock: - config_data = self.__config_data[config_type.value] - if key == "": + config_data = self.__config_data[key.config_type.value] + if key.key == "": return config_data - keys = key.split('.') + keys = key.key.split('.') for k in keys[:-1]: config_data = config_data.get(k, None) if config_data is None: @@ -139,24 +129,23 @@ class ConfigManager: def set( self, - config_type: ConfigType, - key: str = "", + key: ConfigPath, value: Any = None ): with self.__config_lock: - root_data = self.__config_data[config_type.value] - if key == "": - self.__config_data[config_type.value] = value + root_data = self.__config_data[key.config_type.value] + if key.key == "": + self.__config_data[key.config_type.value] = value else: - keys = key.split('.') + keys = key.key.split('.') config_data = root_data for k in keys[:-1]: if k not in config_data: config_data[k] = {} config_data = config_data[k] config_data[keys[-1]] = value - self.save(config_type) + self.save(key.config_type) def save( diff --git a/src/managers/config/ConfigUtils.py b/src/managers/config/ConfigUtils.py index 8d399aa..7f5c68e 100644 --- a/src/managers/config/ConfigUtils.py +++ b/src/managers/config/ConfigUtils.py @@ -11,6 +11,8 @@ import os import managers.config.ConfigManager as ConfigManager +from interfaces.ConfigProvider import CfgKey + class ConfigUtils: """ Config utilities class. @@ -29,7 +31,7 @@ class ConfigUtils: cfg_mgr = ConfigManager.instance() # config manager instance config_paths = {"run": "", "user": ""} - auto_config = cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation", {}) + auto_config = cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.ROOT, {}) for cfg_type in ["run", "user"]: paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", []) index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0) @@ -42,5 +44,5 @@ class ConfigUtils: config_paths[cfg_type] = paths[index] data = {"current": index, "paths": paths} auto_config[f"{cfg_type}_path"] = data - cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation", auto_config) + cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.ROOT, auto_config) return config_paths