| Philipp Le | 6203e73 | 2022-04-26 21:50:26 +0200 | [diff] [blame] | 1 | # SPDX-License-Identifier: MPL-2.0 |
| 2 | # Copyright (c) 2022 Philipp Le <philipp@philipple.de>. |
| 3 | # This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import toml |
| 10 | import json |
| Philipp Le | 4071d91 | 2022-04-26 22:02:43 +0200 | [diff] [blame] | 11 | from typing import Dict, AnyStr, Callable, Type, Any, Optional, List |
| Philipp Le | 6203e73 | 2022-04-26 21:50:26 +0200 | [diff] [blame] | 12 | from pydantic import BaseModel, PrivateAttr |
| 13 | import appdirs |
| 14 | import os |
| 15 | import logging |
| Philipp Le | 4071d91 | 2022-04-26 22:02:43 +0200 | [diff] [blame] | 16 | from tkinter import ttk |
| 17 | from abc import abstractmethod, ABC |
| Philipp Le | 6203e73 | 2022-04-26 21:50:26 +0200 | [diff] [blame] | 18 | |
| 19 | |
| Philipp Le | 4071d91 | 2022-04-26 22:02:43 +0200 | [diff] [blame] | 20 | class ConfigControlFrame(ttk.Frame): |
| 21 | ctrl_widgets: List[ttk.Widget] |
| 22 | |
| 23 | def __init__(self, *args, **kwargs): |
| 24 | super().__init__(*args, **kwargs) |
| 25 | self.ctrl_widgets = [] |
| 26 | |
| 27 | def add_widget(self, w: ttk.Widget) -> None: |
| 28 | self.ctrl_widgets.append(w) |
| 29 | |
| 30 | def widgets_on_change(self, cb) -> None: |
| 31 | for w in self.ctrl_widgets: |
| 32 | w.listen_change(cb) |
| 33 | |
| 34 | |
| 35 | class ConfigObject(BaseModel, ABC): |
| Philipp Le | 6203e73 | 2022-04-26 21:50:26 +0200 | [diff] [blame] | 36 | _KEY: AnyStr = PrivateAttr() |
| 37 | |
| 38 | @classmethod |
| 39 | def get_key(cls) -> AnyStr: |
| 40 | return cls._KEY |
| 41 | |
| Philipp Le | 4071d91 | 2022-04-26 22:02:43 +0200 | [diff] [blame] | 42 | @abstractmethod |
| 43 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 44 | ... |
| 45 | |
| Philipp Le | 6203e73 | 2022-04-26 21:50:26 +0200 | [diff] [blame] | 46 | |
| 47 | class Store: |
| 48 | __instances: Dict[str, Store] = {} |
| 49 | |
| 50 | def __init__(self, raw_objs: Dict[AnyStr, Any], save_cb: Optional[Callable[[Store], None]] = None): |
| 51 | self._raw_objs: Dict[AnyStr, Any] = raw_objs |
| 52 | self._cfg_objs: Dict[AnyStr, ConfigObject] = {} |
| 53 | self._save_cb: Optional[Callable[[Store], None]] = save_cb |
| 54 | |
| 55 | @staticmethod |
| 56 | def get_instance(name: str = 'default') -> Store: |
| 57 | if name in Store.__instances: |
| 58 | return Store.__instances[name] |
| 59 | else: |
| 60 | raise KeyError(f'The Store instance {name} does not exist.') |
| 61 | |
| 62 | @classmethod |
| 63 | def load_obj(cls, obj: Dict[AnyStr, Any], name: str = 'default', *args, **kwargs) -> Store: |
| 64 | if name not in Store.__instances: |
| 65 | Store.__instances[name] = cls(obj, *args, **kwargs) |
| 66 | return Store.__instances[name] |
| 67 | else: |
| 68 | raise KeyError(f'The Store instance {name} already exists.') |
| 69 | |
| 70 | @classmethod |
| 71 | def new(cls, name: str = 'default', *args, **kwargs) -> Store: |
| 72 | if name not in Store.__instances: |
| 73 | Store.__instances[name] = cls({}, *args, **kwargs) |
| 74 | return Store.__instances[name] |
| 75 | else: |
| 76 | raise KeyError(f'The Store instance {name} already exists.') |
| 77 | |
| 78 | def dump_obj(self) -> Dict[AnyStr, Any]: |
| 79 | obj: Dict[AnyStr, Any] = self._raw_objs |
| 80 | for key, item in self._cfg_objs.items(): |
| 81 | obj[key] = json.loads(item.json()) |
| 82 | return obj |
| 83 | |
| 84 | def get_config(self, tp: Type[ConfigObject]): |
| 85 | key = tp.get_key() |
| 86 | if key not in self._cfg_objs: |
| 87 | if key in self._raw_objs: |
| 88 | self._cfg_objs[key] = tp.parse_obj(self._raw_objs[key]) |
| 89 | else: |
| 90 | self._cfg_objs[key] = tp() |
| 91 | return self._cfg_objs[key] |
| 92 | |
| 93 | def save(self) -> None: |
| 94 | if self._save_cb is not None: |
| 95 | self._save_cb(self) |
| 96 | |
| 97 | |
| 98 | class TomlStore(Store): |
| 99 | def __init__(self, raw_objs: Dict[AnyStr, Any], toml_file: str): |
| 100 | super().__init__(raw_objs, save_cb=self._do_save) |
| 101 | self.toml_file: str = toml_file |
| 102 | |
| 103 | @classmethod |
| 104 | def load_or_create_toml(cls, toml_file: str, name: str = 'default') -> TomlStore: |
| 105 | try: |
| 106 | s = Store.get_instance(name) |
| 107 | except KeyError: |
| 108 | try: |
| 109 | s = TomlStore.load_toml(toml_file, name) |
| 110 | logging.info(f'Loaded config from {toml_file}') |
| 111 | except FileNotFoundError: |
| 112 | s = TomlStore.new(name, toml_file=toml_file) |
| 113 | logging.info(f'New config at {toml_file}') |
| 114 | return s |
| 115 | |
| 116 | @classmethod |
| 117 | def load_toml(cls, toml_file: str, name: str = 'default') -> TomlStore: |
| 118 | obj = toml.load(toml_file) |
| 119 | return TomlStore.load_obj(obj, name, toml_file=toml_file) |
| 120 | |
| 121 | def dump_toml(self) -> None: |
| 122 | base_dir = os.path.dirname(self.toml_file) |
| 123 | if not os.path.exists(base_dir): |
| 124 | logging.info(f'Creating directory {base_dir}') |
| 125 | os.mkdir(base_dir) |
| 126 | with open(self.toml_file, 'w', encoding='utf-8') as f: |
| 127 | toml.dump(self.dump_obj(), f) |
| 128 | |
| 129 | def _do_save(self, s: Store) -> None: |
| 130 | assert id(self) == id(s) |
| 131 | self.dump_toml() |
| 132 | |
| 133 | |
| 134 | DEFAULT_PATH = os.path.join( |
| 135 | appdirs.user_config_dir('dcs_interactive', 'Philipp Le'), |
| 136 | 'config.toml' |
| 137 | ) |
| 138 | |
| 139 | |
| 140 | def default_store() -> TomlStore: |
| 141 | return TomlStore.load_or_create_toml(DEFAULT_PATH, 'default') |
| 142 | |