"""Module for config management."""
import dataclasses
import json
import threading
import time
import typing as t
from enum import IntEnum
from pathlib import Path
import aqt
from czech_plus._vendor.loguru import logger
from czech_plus.utils import Singleton
if t.TYPE_CHECKING:
import typing_extensions as te
[docs]BASE_DIR = Path(__file__).parent.parent
[docs]_CONFIG_PATH = BASE_DIR / "config.json"
[docs]_CONFIG_AS_DICT: "te.TypeAlias" = "dict[str, t.Union[str, _CONFIG_AS_DICT]]"
[docs]def _get_anki_config() -> _CONFIG_AS_DICT:
"""Get the config from Anki."""
if aqt.mw is None:
return {}
return t.cast(_CONFIG_AS_DICT, aqt.mw.addonManager.getConfig(BASE_DIR.stem))
[docs]class LogLevel(IntEnum):
"""Log level for the addon."""
"""Use only for tracing error without a debugger."""
@dataclasses.dataclass(frozen=True)
[docs]class LogSettings:
"""Settings for logs."""
[docs] level: LogLevel = LogLevel.WARNING
"""Log level for the app."""
"""Upload logs into JSON."""
@dataclasses.dataclass(frozen=True)
[docs]class BaseCardFields:
"""Base class for card fields."""
"""Name of the field, where czech word is."""
[docs] processed: str = "Processed"
"""Name of the field, where already processed card is."""
@dataclasses.dataclass(frozen=True)
[docs]class NounCardFields(BaseCardFields):
"""Additional fields in noun cards."""
"""Name of the field, where gender is."""
@dataclasses.dataclass(frozen=True)
[docs]class VerbCardFields(BaseCardFields):
"""Additional fields in verb cards."""
[docs] prepositions_and_cases: str = "Prepositions and Cases"
"""Name of the field, where prepositions and cases is."""
@dataclasses.dataclass(frozen=True)
[docs]class AdjectiveCardFields(BaseCardFields):
"""Additional fields in adjective cards."""
[docs] completion_of_comparison_degrees: str = "Completion of Comparison Degrees"
"""Name of the field, where completion of comparison degrees is."""
@dataclasses.dataclass(frozen=True)
[docs]class NounCardsSettings:
"""Settings for noun cards."""
[docs] note_type_name: str = "Noun"
"""Name of the Note Type for nouns."""
[docs] fields: NounCardFields = NounCardFields()
"""Settings for fields in noun cards."""
@dataclasses.dataclass(frozen=True)
[docs]class VerbCardsSettings:
"""Settings for verb cards."""
[docs] note_type_name: str = "Verb"
"""Name of the Note Type for verbs."""
[docs] fields: VerbCardFields = VerbCardFields()
"""Settings for fields in verb cards."""
@dataclasses.dataclass(frozen=True)
[docs]class AdjectivesCardsSettings:
"""Settings for adjective cards."""
[docs] note_type_name: str = "Adjective"
"""Name of the Note Type for adjectives."""
[docs] fields: AdjectiveCardFields = AdjectiveCardFields()
"""Settings for fields in adjective cards."""
@dataclasses.dataclass(frozen=True)
[docs]class CardsSettings:
"""Settings for cards."""
[docs] nouns: NounCardsSettings = NounCardsSettings()
"""Settings for noun cards."""
[docs] verbs: VerbCardsSettings = VerbCardsSettings()
"""Settings for verb cards."""
[docs] adjectives: AdjectivesCardsSettings = AdjectivesCardsSettings()
"""Settings for adjective cards."""
@dataclasses.dataclass(frozen=True)
[docs]class Config(metaclass=Singleton):
"""Config for the addon."""
[docs] logging: LogSettings = LogSettings()
"""Settings for logs."""
[docs] cards: CardsSettings = CardsSettings()
"""Settings for cards."""
[docs] def __post_init__(self) -> None:
"""Post init hook."""
self._setup()
self._start_watching_for_changes()
[docs] def _setup(self) -> None:
"""Perform setup of the config."""
self._write_config()
config = _get_anki_config()
self._set_values(self, config)
# special handling for enums
if isinstance(self.logging.level, str): # type: ignore[unreachable]
object.__setattr__(self.logging, "level", LogLevel[self.logging.level]) # type: ignore[unreachable]
[docs] def _write_config(self) -> None:
"""Write config to the file."""
config = t.cast(_CONFIG_AS_DICT, dataclasses.asdict(self))
config["logging"]["level"] = config["logging"]["level"].name # type: ignore[index,union-attr]
with _CONFIG_PATH.open("w", encoding="utf8") as config_file:
config_file.write(json.dumps(config, indent=4, ensure_ascii=False))
[docs] def _set_values(self, object_to_set: t.Any, config: _CONFIG_AS_DICT, /) -> None: # type: ignore[misc] # Explicit "Any" is not allowed
"""Set values from dict config to object.
We use this method of setting attributes because we use frozen
dataclass. This was found on https://github.com/python/cpython/issues/82625.
Args:
object_to_set: Object to set values to. To support recursion.
config: Dict config to set values from.
"""
for key, value in config.items():
if isinstance(value, dict):
self._set_values(getattr(object_to_set, key), value)
continue
object.__setattr__(object_to_set, key, value)
@classmethod
[docs] def _start_watching_for_changes(cls) -> None:
"""Start watching for changes in config.
This ensures that we will never start two config watchers in one time.
"""
if not hasattr(cls, "_watcher"):
logger.trace("Watcher wasn't started yet, starting it now.")
cls._watcher: threading.Thread = threading.Thread(target=lambda: cls._watch_for_changes(cls()), daemon=True) # type: ignore[misc,attr-defined]
cls._watcher.start() # type: ignore[attr-defined]
else:
logger.trace("Watcher was started before.")
[docs] def _watch_for_changes(self) -> None:
"""Watch for changes in config file and update ``self`` based on changes."""
logger.debug("Start watching for changes in config file.")
stamp = _ADDON_META_PATH.stat().st_mtime
while True:
new_stamp = _ADDON_META_PATH.stat().st_mtime
if new_stamp != stamp:
logger.info("Config file changed. Reloading it.")
stamp = new_stamp
self._setup()
time.sleep(1)