config.py 8.8 KB
Newer Older
之一Yo's avatar
之一Yo 已提交
1 2 3 4 5 6 7
# coding:utf-8
import json
from enum import Enum
from pathlib import Path
from typing import Iterable, List, Union

import darkdetect
之一Yo's avatar
之一Yo 已提交
8
from PyQt5.QtCore import QObject, pyqtSignal
之一Yo's avatar
之一Yo 已提交
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
from PyQt5.QtGui import QColor

from .exception_handler import exceptionHandler


class ConfigValidator:
    """ Config validator """

    def validate(self, value) -> bool:
        """ Verify whether the value is legal """
        return True

    def correct(self, value):
        """ correct illegal value """
        return value


class RangeValidator(ConfigValidator):
    """ Range validator """

    def __init__(self, min, max):
        self.min = min
        self.max = max
        self.range = (min, max)

    def validate(self, value) -> bool:
        return self.min <= value <= self.max

    def correct(self, value):
        return min(max(self.min, value), self.max)


class OptionsValidator(ConfigValidator):
    """ Options validator """

    def __init__(self, options: Union[Iterable, Enum]) -> None:
        if not options:
            raise ValueError("The `options` can't be empty.")

        if isinstance(options, Enum):
            options = options._member_map_.values()

        self.options = list(options)

    def validate(self, value) -> bool:
        return value in self.options

    def correct(self, value):
        return value if self.validate(value) else self.options[0]


class BoolValidator(OptionsValidator):
    """ Boolean validator """

    def __init__(self):
        super().__init__([True, False])


class FolderValidator(ConfigValidator):
    """ Folder validator """

    def validate(self, value: str) -> bool:
        return Path(value).exists()

    def correct(self, value: str):
        path = Path(value)
        path.mkdir(exist_ok=True, parents=True)
        return str(path.absolute()).replace("\\", "/")


class FolderListValidator(ConfigValidator):
    """ Folder list validator """

    def validate(self, value: List[str]) -> bool:
        return all(Path(i).exists() for i in value)

    def correct(self, value: List[str]):
        folders = []
        for folder in value:
            path = Path(folder)
            if path.exists():
                folders.append(str(path.absolute()).replace("\\", "/"))

        return folders


class ColorValidator(ConfigValidator):
    """ RGB color validator """

    def __init__(self, default):
        self.default = QColor(default)

    def validate(self, color) -> bool:
        try:
            return QColor(color).isValid()
        except:
            return False

    def correct(self, value):
        return QColor(value) if self.validate(value) else self.default


class ConfigSerializer:
    """ Config serializer """

    def serialize(self, value):
        """ serialize config value """
        return value

    def deserialize(self, value):
        """ deserialize config from config file's value """
        return value


class EnumSerializer(ConfigSerializer):
    """ enumeration class serializer """

    def __init__(self, enumClass):
        self.enumClass = enumClass

    def serialize(self, value: Enum):
        return value.value

    def deserialize(self, value):
        return self.enumClass(value)


class ColorSerializer(ConfigSerializer):
    """ QColor serializer """

    def serialize(self, value: QColor):
        return value.name()

    def deserialize(self, value):
        if isinstance(value, list):
            return QColor(*value)

        return QColor(value)


class ConfigItem:
    """ Config item """

    def __init__(self, group: str, name: str, default, validator: ConfigValidator = None,
之一Yo's avatar
之一Yo 已提交
153
                 serializer: ConfigSerializer = None, restart=False):
之一Yo's avatar
之一Yo 已提交
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
        """
        Parameters
        ----------
        group: str
            config group name

        name: str
            config item name, can be empty

        default:
            default value

        options: list
            options value

        serializer: ConfigSerializer
            config serializer
之一Yo's avatar
之一Yo 已提交
171 172 173

        restart: bool
            whether to restart the application after updating value
之一Yo's avatar
之一Yo 已提交
174 175 176 177 178 179 180
        """
        self.group = group
        self.name = name
        self.validator = validator or ConfigValidator()
        self.serializer = serializer or ConfigSerializer()
        self.__value = default
        self.value = default
之一Yo's avatar
之一Yo 已提交
181
        self.restart = restart
之一Yo's avatar
之一Yo 已提交
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196

    @property
    def value(self):
        """ get the value of config item """
        return self.__value

    @value.setter
    def value(self, v):
        self.__value = self.validator.correct(v)

    @property
    def key(self):
        """ get the config key separated by `.` """
        return self.group+"."+self.name if self.name else self.group

之一Yo's avatar
之一Yo 已提交
197 198 199
    def __str__(self) -> str:
        return f'{self.__class__.__name__}[value={self.value}]'

之一Yo's avatar
之一Yo 已提交
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    def serialize(self):
        return self.serializer.serialize(self.value)

    def deserializeFrom(self, value):
        self.value = self.serializer.deserialize(value)


class RangeConfigItem(ConfigItem):
    """ Config item of range """

    @property
    def range(self):
        """ get the available range of config """
        return self.validator.range

之一Yo's avatar
之一Yo 已提交
215 216 217
    def __str__(self) -> str:
        return f'{self.__class__.__name__}[range={self.range}, value={self.value}]'

之一Yo's avatar
之一Yo 已提交
218 219 220 221 222 223 224 225

class OptionsConfigItem(ConfigItem):
    """ Config item with options """

    @property
    def options(self):
        return self.validator.options

之一Yo's avatar
之一Yo 已提交
226 227 228
    def __str__(self) -> str:
        return f'{self.__class__.__name__}[options={self.options}, value={self.value}]'

之一Yo's avatar
之一Yo 已提交
229 230 231 232

class ColorConfigItem(ConfigItem):
    """ Color config item """

之一Yo's avatar
之一Yo 已提交
233 234 235 236 237 238
    def __init__(self, group: str, name: str, default, restart=False):
        super().__init__(group, name, QColor(default), ColorValidator(default),
                         ColorSerializer(), restart)

    def __str__(self) -> str:
        return f'{self.__class__.__name__}[value={self.value.name()}]'
之一Yo's avatar
之一Yo 已提交
239 240


之一Yo's avatar
之一Yo 已提交
241
class QConfig(QObject):
之一Yo's avatar
之一Yo 已提交
242 243
    """ Config of app """

之一Yo's avatar
之一Yo 已提交
244
    appRestartSig = pyqtSignal()
之一Yo's avatar
之一Yo 已提交
245 246 247 248 249

    themeMode = OptionsConfigItem(
        "MainWindow", "Theme", "Light", OptionsValidator(["Light", "Dark", "Auto"]))

    def __init__(self):
之一Yo's avatar
之一Yo 已提交
250 251
        super().__init__()
        self.file = Path("config/config.json")
之一Yo's avatar
之一Yo 已提交
252
        self._theme = "Light"
之一Yo's avatar
之一Yo 已提交
253
        self._cfg = self
之一Yo's avatar
之一Yo 已提交
254

之一Yo's avatar
之一Yo 已提交
255
    def get(self, item: ConfigItem):
之一Yo's avatar
之一Yo 已提交
256 257
        return item.value

之一Yo's avatar
之一Yo 已提交
258
    def set(self, item: ConfigItem, value):
之一Yo's avatar
之一Yo 已提交
259 260 261 262
        if item.value == value:
            return

        item.value = value
之一Yo's avatar
之一Yo 已提交
263 264 265 266
        self.save()

        if item.restart:
            self._cfg.appRestartSig.emit()
之一Yo's avatar
之一Yo 已提交
267

之一Yo's avatar
之一Yo 已提交
268
    def toDict(self, serialize=True):
之一Yo's avatar
之一Yo 已提交
269 270
        """ convert config items to `dict` """
        items = {}
之一Yo's avatar
之一Yo 已提交
271 272
        for name in dir(self._cfg.__class__):
            item = getattr(self._cfg.__class__, name)
之一Yo's avatar
之一Yo 已提交
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
            if not isinstance(item, ConfigItem):
                continue

            value = item.serialize() if serialize else item.value
            if not items.get(item.group):
                if not item.name:
                    items[item.group] = value
                else:
                    items[item.group] = {}

            if item.name:
                items[item.group][item.name] = value

        return items

之一Yo's avatar
之一Yo 已提交
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    def save(self):
        self._cfg.file.parent.mkdir(parents=True, exist_ok=True)
        with open(self._cfg.file, "w", encoding="utf-8") as f:
            json.dump(self._cfg.toDict(), f, ensure_ascii=False, indent=4)

    @exceptionHandler()
    def load(self, file=None, config=None):
        """ load config

        Parameters
        ----------
        file: str or Path
            the path of json config file

        config: Config
            config object to be initialized
        """
        if isinstance(config, QConfig):
            self._cfg = config
之一Yo's avatar
之一Yo 已提交
307

之一Yo's avatar
之一Yo 已提交
308 309
        if isinstance(file, (str, Path)):
            self._cfg.file = Path(file)
之一Yo's avatar
之一Yo 已提交
310 311

        try:
之一Yo's avatar
之一Yo 已提交
312
            with open(self._cfg.file, encoding="utf-8") as f:
之一Yo's avatar
之一Yo 已提交
313 314 315 316 317 318
                cfg = json.load(f)
        except:
            cfg = {}

        # map config items'key to item
        items = {}
之一Yo's avatar
之一Yo 已提交
319 320
        for name in dir(self._cfg.__class__):
            item = getattr(self._cfg.__class__, name)
之一Yo's avatar
之一Yo 已提交
321 322 323 324 325 326 327 328 329 330 331 332 333
            if isinstance(item, ConfigItem):
                items[item.key] = item

        # update the value of config item
        for k, v in cfg.items():
            if not isinstance(v, dict) and items.get(k) is not None:
                items[k].deserializeFrom(v)
            elif isinstance(v, dict):
                for key, value in v.items():
                    key = k + "." + key
                    if items.get(key) is not None:
                        items[key].deserializeFrom(value)

之一Yo's avatar
之一Yo 已提交
334 335
        if self.get(self._cfg.themeMode) == "Auto":
            self._cfg._theme = darkdetect.theme() or "Light"
之一Yo's avatar
之一Yo 已提交
336
        else:
之一Yo's avatar
之一Yo 已提交
337
            self._cfg._theme = self.get(self._cfg.themeMode)
之一Yo's avatar
之一Yo 已提交
338 339 340 341

    @property
    def theme(self):
        """ get theme mode, can be `light` or `dark` """
之一Yo's avatar
之一Yo 已提交
342
        return self._cfg._theme.lower()
之一Yo's avatar
之一Yo 已提交
343 344


之一Yo's avatar
之一Yo 已提交
345
qconfig = QConfig()