From eca421e0f2079ad8992fbaa5956a59d314a9c8a0 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 13 Dec 2022 08:50:38 -0800 Subject: [PATCH] Split out settings functions --- comictaggerlib/settings/__init__.py | 4 +- comictaggerlib/settings/manager.py | 303 ++++++++++++++++++---------- comictaggerlib/settingswindow.py | 8 +- comictaggerlib/taggerwindow.py | 6 +- 4 files changed, 214 insertions(+), 107 deletions(-) diff --git a/comictaggerlib/settings/__init__.py b/comictaggerlib/settings/__init__.py index a7c069b..c4c4a90 100644 --- a/comictaggerlib/settings/__init__.py +++ b/comictaggerlib/settings/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from comictaggerlib.settings.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options from comictaggerlib.settings.file import register_settings, validate_settings -from comictaggerlib.settings.manager import Manager, OptionDefinitions, OptionValues +from comictaggerlib.settings.manager import Manager, OptionDefinitions, OptionValues, defaults, save_file from comictaggerlib.settings.types import ComicTaggerPaths __all__ = [ @@ -15,4 +15,6 @@ __all__ = [ "ComicTaggerPaths", "OptionValues", "OptionDefinitions", + "save_file", + "defaults", ] diff --git a/comictaggerlib/settings/manager.py b/comictaggerlib/settings/manager.py index bd65e8e..fa34606 100644 --- a/comictaggerlib/settings/manager.py +++ b/comictaggerlib/settings/manager.py @@ -6,7 +6,7 @@ import logging import pathlib from collections import defaultdict from collections.abc import Sequence -from typing import Any, Callable, NoReturn, Union +from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union logger = logging.getLogger(__name__) @@ -115,7 +115,162 @@ class Setting: OptionValues = dict[str, dict[str, Any]] OptionDefinitions = dict[str, dict[str, Setting]] -ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser] +if TYPE_CHECKING: + ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser] + + +def get_option(options: OptionValues | argparse.Namespace, setting: Setting) -> tuple[Any, bool]: + """ + Helper function to retrieve the value for a setting and if the value is the default value + + Args: + options: Dictionary or namespace of options + setting: The setting object describing the value to retrieve + """ + if isinstance(options, dict): + value = options.get(setting.group, {}).get(setting.dest, setting.default) + else: + value = getattr(options, setting.internal_name, setting.default) + return value, value == setting.default + + +def normalize_options( + raw_options: OptionValues | argparse.Namespace, + definitions: OptionDefinitions, + file: bool = False, + cmdline: bool = False, + defaults: bool = True, + raw_options_2: OptionValues | argparse.Namespace | None = None, +) -> OptionValues: + """ + Creates an `OptionValues` dictionary with setting definitions taken from `self.definitions` + and values taken from `raw_options` and `raw_options_2' if defined. + Values are assigned so if the value is a dictionary mutating it will mutate the original. + + Args: + raw_options: The dict or Namespace to normalize options from + definitions: The definition of the options + file: Include file options + cmdline: Include cmdline options + defaults: Include default values in the returned dict + raw_options_2: If set, merges non-default values into the returned dict + """ + options: OptionValues = {} + for group_name, group in definitions.items(): + group_options = {} + for setting_name, setting in group.items(): + if (setting.cmdline and cmdline) or (setting.file and file): + # Ensures the option exists with the default if not already set + value, default = get_option(raw_options, setting) + if not default or default and defaults: + group_options[setting_name] = value + + # will override with option from raw_options_2 if it is not the default + if raw_options_2 is not None: + value, default = get_option(raw_options_2, setting) + if not default: + group_options[setting_name] = value + options[group_name] = group_options + options["definitions"] = definitions + return options + + +def parse_file(filename: pathlib.Path, definitions: OptionDefinitions) -> OptionValues: + """ + Helper function to read options from a json dictionary from a file + Args: + filename: A pathlib.Path object to read a json dictionary from + """ + options: OptionValues = {} + if filename.exists(): + try: + with filename.open() as file: + opts = json.load(file) + if isinstance(opts, dict): + options = opts + except Exception: + logger.exception("Failed to load config file: %s", filename) + else: + logger.info("No config file found") + + return normalize_options(options, definitions, file=True) + + +def clean_options( + options: OptionValues | argparse.Namespace, definitions: OptionDefinitions, file: bool = False, cmdline: bool = True +) -> OptionValues: + """ + Normalizes options and then cleans up empty groups and removes 'definitions' + Args: + options: + file: + cmdline: + + Returns: + + """ + + clean_options = normalize_options(options, definitions, file=file, cmdline=cmdline) + del clean_options["definitions"] + for group in list(clean_options.keys()): + if not clean_options[group]: + del clean_options[group] + return clean_options + + +def defaults(definitions: OptionDefinitions) -> OptionValues: + return normalize_options({}, definitions, file=True, cmdline=True) + + +def get_namespace(options: OptionValues, definitions: OptionDefinitions, defaults: bool = True) -> argparse.Namespace: + """ + Returns an argparse.Namespace object with options in the form "{group_name}_{setting_name}" + `options` should already be normalized. + Throws an exception if the internal_name is duplicated + + Args: + options: Normalized options to turn into a Namespace + defaults: Include default values in the returned dict + """ + options = normalize_options(options, definitions, file=True, cmdline=True) + namespace = argparse.Namespace() + for group_name, group in definitions.items(): + for setting_name, setting in group.items(): + if hasattr(namespace, setting.internal_name): + raise Exception(f"Duplicate internal name: {setting.internal_name}") + value, default = get_option(options, setting) + + if not default or default and defaults: + setattr( + namespace, + setting.internal_name, + value, + ) + setattr(namespace, "definitions", definitions) + return namespace + + +def save_file( + options: OptionValues | argparse.Namespace, definitions: OptionDefinitions, filename: pathlib.Path +) -> bool: + """ + Helper function to save options from a json dictionary to a file + Args: + options: The options to save to a json dictionary + filename: A pathlib.Path object to save the json dictionary to + """ + file_options = clean_options(options, definitions, file=True) + if not filename.exists(): + filename.parent.mkdir(exist_ok=True, parents=True) + filename.touch() + + try: + json_str = json.dumps(file_options, indent=2) + filename.write_text(json_str, encoding="utf-8") + except Exception: + logger.exception("Failed to save config file: %s", filename) + return False + return True class Manager: @@ -129,41 +284,17 @@ class Manager: self.description = description self.epilog = epilog - self.option_definitions: OptionDefinitions = defaultdict(lambda: dict()) + self.definitions: OptionDefinitions = defaultdict(lambda: dict()) if definitions: - self.option_definitions = definitions + self.definitions = definitions self.exclusive_group = False self.current_group_name = "" - def defaults(self) -> OptionValues: - return self.normalize_options({}, file=True, cmdline=True) - - def get_namespace(self, options: OptionValues, defaults: bool = True) -> argparse.Namespace: - """ - Returns an argparse.Namespace object with options in the form "{group_name}_{setting_name}" - `options` should already be normalized. - Throws an exception if the internal_name is duplicated - """ - namespace = argparse.Namespace() - for group_name, group in self.option_definitions.items(): - for setting_name, setting in group.items(): - if hasattr(namespace, setting.internal_name): - raise Exception(f"Duplicate internal name: {setting.internal_name}") - value, default = self.get_option(options, setting, group_name) - if not default or default and defaults: - setattr( - namespace, - setting.internal_name, - value, - ) - setattr(namespace, "option_definitions", options.get("option_definitions")) - return namespace - def add_setting(self, *args: Any, **kwargs: Any) -> None: """Takes passes all arguments through to `Setting`, `group` and `exclusive` are already set""" setting = Setting(*args, group=self.current_group_name, exclusive=self.exclusive_group, **kwargs) - self.option_definitions[self.current_group_name][setting.dest] = setting + self.definitions[self.current_group_name][setting.dest] = setting def create_argparser(self) -> None: """Creates an :class:`argparse.ArgumentParser` from all cmdline settings""" @@ -173,7 +304,7 @@ class Manager: epilog=self.epilog, formatter_class=argparse.RawTextHelpFormatter, ) - for group_name, group in self.option_definitions.items(): + for group_name, group in self.definitions.items(): for setting_name, setting in group.items(): if setting.cmdline: argparse_args, argparse_kwargs = setting.to_argparse() @@ -193,6 +324,14 @@ class Manager: current_group.add_argument(*argparse_args, **argparse_kwargs) def add_group(self, name: str, add_settings: Callable[[Manager], None], exclusive_group: bool = False) -> None: + """ + The primary way to add define options on this class + + Args: + name: The name of the group to define + add_settings: A function that registers individual options using :meth:`add_setting` + exclusive_group: If this group is an argparse exclusive group + """ self.current_group_name = name self.exclusive_group = exclusive_group add_settings(self) @@ -200,47 +339,17 @@ class Manager: self.exclusive_group = False def exit(self, *args: Any, **kwargs: Any) -> NoReturn: - """Same as :class:`~argparse.ArgumentParser`""" + """See :class:`~argparse.ArgumentParser`""" self.argparser.exit(*args, **kwargs) raise SystemExit(99) - def save_file(self, options: OptionValues | argparse.Namespace, filename: pathlib.Path) -> bool: - if isinstance(options, dict): - self.option_definitions = options["option_definitions"] - elif isinstance(options, argparse.Namespace): - self.option_definitions = options.option_definitions + def defaults(self) -> OptionValues: + return defaults(self.definitions) - file_options = self.normalize_options(options, file=True) - del file_options["option_definitions"] - for group in list(file_options.keys()): - if not file_options[group]: - del file_options[group] - if not filename.exists(): - filename.parent.mkdir(exist_ok=True, parents=True) - filename.touch() - - try: - json_str = json.dumps(file_options, indent=2) - filename.write_text(json_str, encoding="utf-8") - except Exception: - logger.exception("Failed to save config file: %s", filename) - return False - return True - - def parse_file(self, filename: pathlib.Path) -> OptionValues: - options: OptionValues = {} - if filename.exists(): - try: - with filename.open() as file: - opts = json.load(file) - if isinstance(opts, dict): - options = opts - except Exception: - logger.exception("Failed to load config file: %s", filename) - else: - logger.info("No config file found") - - return self.normalize_options(options, file=True) + def clean_options( + self, options: OptionValues | argparse.Namespace, file: bool = False, cmdline: bool = True + ) -> OptionValues: + return clean_options(options=options, definitions=self.definitions, file=file, cmdline=cmdline) def normalize_options( self, @@ -250,53 +359,43 @@ class Manager: defaults: bool = True, raw_options_2: OptionValues | argparse.Namespace | None = None, ) -> OptionValues: - """ - Creates an `OptionValues` dictionary with setting definitions taken from `self.option_definitions` - and values taken from `raw_options` and `raw_options_2' if defined. - Values are assigned so if the value is a dictionary mutating it will mutate the original. - """ - options: OptionValues = {} - for group_name, group in self.option_definitions.items(): - group_options = {} - for setting_name, setting in group.items(): - if (setting.cmdline and cmdline) or (setting.file and file): - # Ensures the option exists with the default if not already set - value, default = self.get_option(raw_options, setting, group_name) - if not default or default and defaults: - group_options[setting_name] = value + return normalize_options( + raw_options=raw_options, + definitions=self.definitions, + file=file, + cmdline=cmdline, + defaults=defaults, + raw_options_2=raw_options_2, + ) - # will override with option from raw_options_2 if it is not the default - if raw_options_2 is not None: - value, default = self.get_option(raw_options_2, setting, group_name) - if not default: - group_options[setting_name] = value - options[group_name] = group_options - options["option_definitions"] = self.option_definitions - return options + def get_namespace(self, options: OptionValues, defaults: bool = True) -> argparse.Namespace: + return get_namespace(options=options, definitions=self.definitions, defaults=defaults) - def get_option( - self, options: OptionValues | argparse.Namespace, setting: Setting, group_name: str - ) -> tuple[Any, bool]: - """Helper function to retrieve the the value for a setting and if the value is the default value""" - if isinstance(options, dict): - value = options.get(group_name, {}).get(setting.dest, setting.default) - else: - value = getattr(options, setting.internal_name, setting.default) - return value, value == setting.default + def parse_file(self, filename: pathlib.Path) -> OptionValues: + return parse_file(filename=filename, definitions=self.definitions) + + def save_file(self, options: OptionValues | argparse.Namespace, filename: pathlib.Path) -> bool: + return save_file(options=options, definitions=self.definitions, filename=filename) def parse_args(self, args: list[str] | None = None, namespace: argparse.Namespace | None = None) -> OptionValues: """ - Creates an `argparse.ArgumentParser` from cmdline settings in `self.option_definitions`. + Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`. `args` and `namespace` are passed to `argparse.ArgumentParser.parse_args` + + Args: + args: Passed to argparse.ArgumentParser.parse + namespace: Passed to argparse.ArgumentParser.parse """ self.create_argparser() ns = self.argparser.parse_args(args, namespace=namespace) - return self.normalize_options(ns, cmdline=True, file=True) + return normalize_options(ns, definitions=self.definitions, cmdline=True, file=True) def parse_options(self, config_path: pathlib.Path, args: list[str] | None = None) -> OptionValues: - file_options = self.parse_file(config_path) + file_options = self.parse_file( + config_path, + ) cli_options = self.parse_args(args, self.get_namespace(file_options, defaults=False)) - final_options = self.normalize_options(cli_options, file=True, cmdline=True) + final_options = normalize_options(cli_options, definitions=self.definitions, file=True, cmdline=True) return final_options diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 347fc5d..cb164c4 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -438,7 +438,11 @@ class SettingsWindow(QtWidgets.QDialog): self.options["rename"]["strict"] = self.cbxRenameStrict.isChecked() self.options["rename"]["replacements"] = self.get_replacemnts() - settings.Manager().save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json") + settings.save_file( + self.options, + self.options["definitions"], + self.options["runtime"]["config"].user_config_dir / "settings.json", + ) self.parent().options = self.options QtWidgets.QDialog.accept(self) @@ -457,7 +461,7 @@ class SettingsWindow(QtWidgets.QDialog): QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.") def reset_settings(self) -> None: - self.options = settings.Manager(definitions=self.options["option_definitions"]).defaults() + self.options = settings.defaults(self.options["definitions"]) self.settings_to_form() QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.") diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index fb54d41..3ec4f5a 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -2003,8 +2003,10 @@ Have fun! self.options["internal"]["last_filelist_sorted_column"], self.options["internal"]["last_filelist_sorted_order"], ) = self.fileSelectionList.get_sorting() - settings.Manager().save_file( - self.options, self.options["runtime"]["config"].user_config_dir / "settings.json" + settings.save_file( + self.options, + self.options["definitions"], + self.options["runtime"]["config"].user_config_dir / "settings.json", ) event.accept()