Split out settings functions
This commit is contained in:
parent
18566a0592
commit
eca421e0f2
@ -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",
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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.")
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user