Split out settings functions

This commit is contained in:
Timmy Welch 2022-12-13 08:50:38 -08:00
parent 18566a0592
commit eca421e0f2
No known key found for this signature in database
4 changed files with 214 additions and 107 deletions

View File

@ -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",
]

View File

@ -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

View File

@ -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.")

View File

@ -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()