From 103379e5484136328eac97f7d551f52564154c5a Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Wed, 14 Dec 2022 23:13:53 -0800 Subject: [PATCH] Split settings out into a separate package --- comictaggerlib/autotagmatchwindow.py | 4 +- comictaggerlib/autotagstartwindow.py | 4 +- comictaggerlib/cbltransformer.py | 5 +- comictaggerlib/cli.py | 6 +- comictaggerlib/ctoptions/__init__.py | 14 + .../{settings => ctoptions}/cmdline.py | 14 +- .../{settings => ctoptions}/file.py | 27 +- .../{settings => ctoptions}/types.py | 0 comictaggerlib/fileselectionlist.py | 4 +- comictaggerlib/gui.py | 13 +- comictaggerlib/issueidentifier.py | 4 +- comictaggerlib/issueselectionwindow.py | 4 +- comictaggerlib/main.py | 81 ++-- comictaggerlib/matchselectionwindow.py | 4 +- comictaggerlib/renamewindow.py | 36 +- comictaggerlib/settings/__init__.py | 20 - comictaggerlib/settings/manager.py | 401 ------------------ comictaggerlib/settingswindow.py | 166 ++++---- comictaggerlib/taggerwindow.py | 147 +++---- comictaggerlib/volumeselectionwindow.py | 4 +- requirements.txt | 1 + testing/settings.py | 214 ---------- tests/comiccacher_test.py | 6 +- tests/conftest.py | 22 +- tests/issueidentifier_test.py | 16 +- tests/settings_test.py | 152 ------- 26 files changed, 307 insertions(+), 1062 deletions(-) create mode 100644 comictaggerlib/ctoptions/__init__.py rename comictaggerlib/{settings => ctoptions}/cmdline.py (96%) rename comictaggerlib/{settings => ctoptions}/file.py (94%) rename comictaggerlib/{settings => ctoptions}/types.py (100%) delete mode 100644 comictaggerlib/settings/__init__.py delete mode 100644 comictaggerlib/settings/manager.py delete mode 100644 testing/settings.py delete mode 100644 tests/settings_test.py diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 4baf51d..59496e4 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -19,11 +19,11 @@ import logging import os from typing import Callable +import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic from comicapi.comicarchive import MetaDataStyle from comicapi.genericmetadata import GenericMetadata -from comictaggerlib import settings from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.resulttypes import IssueResult, MultipleMatch from comictaggerlib.ui import ui_path @@ -42,7 +42,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): match_set_list: list[MultipleMatch], style: int, fetch_func: Callable[[IssueResult], GenericMetadata], - options: settings.OptionValues, + options: settngs.ConfigValues, talker_api: ComicTalker, ) -> None: super().__init__(parent) diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 4ee1c5f..5b8901c 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -17,16 +17,16 @@ from __future__ import annotations import logging +import settngs from PyQt5 import QtCore, QtWidgets, uic -from comictaggerlib import settings from comictaggerlib.ui import ui_path logger = logging.getLogger(__name__) class AutoTagStartWindow(QtWidgets.QDialog): - def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, msg: str) -> None: + def __init__(self, parent: QtWidgets.QWidget, options: settngs.ConfigValues, msg: str) -> None: super().__init__(parent) uic.loadUi(ui_path / "autotagstartwindow.ui", self) diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index c6d9726..ec0e59d 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -17,14 +17,15 @@ from __future__ import annotations import logging +import settngs + from comicapi.genericmetadata import CreditMetadata, GenericMetadata -from comictaggerlib import settings logger = logging.getLogger(__name__) class CBLTransformer: - def __init__(self, metadata: GenericMetadata, options: settings.OptionValues) -> None: + def __init__(self, metadata: GenericMetadata, options: settngs.ConfigValues) -> None: self.metadata = metadata self.options = options diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index a51bc01..f01dd04 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -23,10 +23,12 @@ import sys from datetime import datetime from pprint import pprint +import settngs + from comicapi import utils from comicapi.comicarchive import ComicArchive, MetaDataStyle from comicapi.genericmetadata import GenericMetadata -from comictaggerlib import ctversion, settings +from comictaggerlib import ctversion from comictaggerlib.cbltransformer import CBLTransformer from comictaggerlib.filerenamer import FileRenamer, get_rename_dir from comictaggerlib.graphics import graphics_path @@ -38,7 +40,7 @@ logger = logging.getLogger(__name__) class CLI: - def __init__(self, options: settings.OptionValues, talker_api: ComicTalker): + def __init__(self, options: settngs.Values, talker_api: ComicTalker): self.options = options self.talker_api = talker_api self.batch_mode = False diff --git a/comictaggerlib/ctoptions/__init__.py b/comictaggerlib/ctoptions/__init__.py new file mode 100644 index 0000000..fd1f386 --- /dev/null +++ b/comictaggerlib/ctoptions/__init__.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from comictaggerlib.ctoptions.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options +from comictaggerlib.ctoptions.file import register_settings, validate_settings +from comictaggerlib.ctoptions.types import ComicTaggerPaths + +__all__ = [ + "initial_cmd_line_parser", + "register_commandline", + "register_settings", + "validate_commandline_options", + "validate_settings", + "ComicTaggerPaths", +] diff --git a/comictaggerlib/settings/cmdline.py b/comictaggerlib/ctoptions/cmdline.py similarity index 96% rename from comictaggerlib/settings/cmdline.py rename to comictaggerlib/ctoptions/cmdline.py index d5920eb..087f972 100644 --- a/comictaggerlib/settings/cmdline.py +++ b/comictaggerlib/ctoptions/cmdline.py @@ -19,13 +19,13 @@ import argparse import logging import os import platform -from typing import Any + +import settngs from comicapi import utils from comicapi.genericmetadata import GenericMetadata from comictaggerlib import ctversion -from comictaggerlib.settings.manager import Manager -from comictaggerlib.settings.types import ComicTaggerPaths, metadata_type, parse_metadata_from_string +from comictaggerlib.ctoptions.types import ComicTaggerPaths, metadata_type, parse_metadata_from_string logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def initial_cmd_line_parser() -> argparse.ArgumentParser: return parser -def register_options(parser: Manager) -> None: +def register_options(parser: settngs.Manager) -> None: parser.add_setting( "--config", help="Config directory defaults to ~/.Config/ComicTagger\non Linux, ~/Library/Application Support/ComicTagger on Mac and %%APPDATA%%\\ComicTagger on Windows\n", @@ -200,7 +200,7 @@ def register_options(parser: Manager) -> None: parser.add_setting("files", nargs="*", file=False) -def register_commands(parser: Manager) -> None: +def register_commands(parser: settngs.Manager) -> None: parser.add_setting( "--version", action="store_true", @@ -259,12 +259,12 @@ def register_commands(parser: Manager) -> None: ) -def register_commandline(parser: Manager) -> None: +def register_commandline(parser: settngs.Manager) -> None: parser.add_group("commands", register_commands, True) parser.add_group("runtime", register_options) -def validate_commandline_options(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]: +def validate_commandline_options(options: settngs.Values, parser: settngs.Manager) -> settngs.Values: if options["commands"]["version"]: parser.exit( diff --git a/comictaggerlib/settings/file.py b/comictaggerlib/ctoptions/file.py similarity index 94% rename from comictaggerlib/settings/file.py rename to comictaggerlib/ctoptions/file.py index 2a528b9..1d50643 100644 --- a/comictaggerlib/settings/file.py +++ b/comictaggerlib/ctoptions/file.py @@ -4,12 +4,13 @@ import argparse import uuid from typing import Any +import settngs + +from comictaggerlib.ctoptions.types import AppendAction from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements -from comictaggerlib.settings.manager import Manager -from comictaggerlib.settings.types import AppendAction -def general(parser: Manager) -> None: +def general(parser: settngs.Manager) -> None: # General Settings parser.add_setting("--rar-exe-path", default="rar", help="The path to the rar program") parser.add_setting( @@ -22,7 +23,7 @@ def general(parser: Manager) -> None: parser.add_setting("send_usage_stats", default=False, cmdline=False) -def internal(parser: Manager) -> None: +def internal(parser: settngs.Manager) -> None: # automatic settings parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False) parser.add_setting("last_selected_save_data_style", default=0, cmdline=False) @@ -38,7 +39,7 @@ def internal(parser: Manager) -> None: parser.add_setting("last_filelist_sorted_order", default=0, cmdline=False) -def identifier(parser: Manager) -> None: +def identifier(parser: settngs.Manager) -> None: # identifier settings parser.add_setting("--series-match-identify-thresh", default=91, type=int, help="") parser.add_setting( @@ -49,7 +50,7 @@ def identifier(parser: Manager) -> None: ) -def dialog(parser: Manager) -> None: +def dialog(parser: settngs.Manager) -> None: # Show/ask dialog flags parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False) parser.add_setting("show_disclaimer", default=True, cmdline=False) @@ -57,7 +58,7 @@ def dialog(parser: Manager) -> None: parser.add_setting("ask_about_usage_stats", default=True, cmdline=False) -def filename(parser: Manager) -> None: +def filename(parser: settngs.Manager) -> None: # filename parsing settings parser.add_setting( "--complicated-parser", @@ -85,7 +86,7 @@ def filename(parser: Manager) -> None: ) -def comicvine(parser: Manager) -> None: +def comicvine(parser: settngs.Manager) -> None: # Comic Vine settings parser.add_setting( "--series-match-search-thresh", @@ -145,7 +146,7 @@ def comicvine(parser: Manager) -> None: ) -def cbl(parser: Manager) -> None: +def cbl(parser: settngs.Manager) -> None: # CBL Transform settings parser.add_setting("--assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction) parser.add_setting("--copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction) @@ -158,7 +159,7 @@ def cbl(parser: Manager) -> None: parser.add_setting("--apply-cbl-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction) -def rename(parser: Manager) -> None: +def rename(parser: settngs.Manager) -> None: # Rename settings parser.add_setting("--template", default="{series} #{issue} ({year})", help="The teplate to use when renaming") parser.add_setting( @@ -199,7 +200,7 @@ def rename(parser: Manager) -> None: ) -def autotag(parser: Manager) -> None: +def autotag(parser: settngs.Manager) -> None: # Auto-tag stickies parser.add_setting( "--save-on-low-confidence", @@ -238,7 +239,7 @@ def autotag(parser: Manager) -> None: ) -def validate_settings(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]: +def validate_settings(options: dict[str, dict[str, Any]], parser: settngs.Manager) -> dict[str, dict[str, Any]]: options["identifier"]["publisher_filter"] = [ x.strip() for x in options["identifier"]["publisher_filter"] if x.strip() ] @@ -249,7 +250,7 @@ def validate_settings(options: dict[str, dict[str, Any]], parser: Manager) -> di return options -def register_settings(parser: Manager) -> None: +def register_settings(parser: settngs.Manager) -> None: parser.add_group("general", general, False) parser.add_group("internal", internal, False) parser.add_group("identifier", identifier, False) diff --git a/comictaggerlib/settings/types.py b/comictaggerlib/ctoptions/types.py similarity index 100% rename from comictaggerlib/settings/types.py rename to comictaggerlib/ctoptions/types.py diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index 7eb0116..5086d05 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -20,11 +20,11 @@ import os import platform from typing import Callable, cast +import settngs from PyQt5 import QtCore, QtWidgets, uic from comicapi import utils from comicapi.comicarchive import ComicArchive -from comictaggerlib import settings from comictaggerlib.graphics import graphics_path from comictaggerlib.optionalmsgdialog import OptionalMessageDialog from comictaggerlib.settingswindow import linuxRarHelp, macRarHelp, windowsRarHelp @@ -59,7 +59,7 @@ class FileSelectionList(QtWidgets.QWidget): def __init__( self, parent: QtWidgets.QWidget, - options: settings.OptionValues, + options: settngs.ConfigValues, dirty_flag_verification: Callable[[str, str], bool], ) -> None: super().__init__(parent) diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 1212744..979a44e 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -7,7 +7,8 @@ import sys import traceback import types -from comictaggerlib import settings +import settngs + from comictaggerlib.graphics import graphics_path from comictalker.talkerbase import ComicTalker @@ -81,12 +82,10 @@ except ImportError as e: qt_available = False -def open_tagger_window( - talker_api: ComicTalker, options: settings.OptionValues, gui_exception: Exception | None -) -> None: +def open_tagger_window(talker_api: ComicTalker, options: settngs.Config, gui_exception: Exception | None) -> None: os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" args = [] - if options["runtime"]["darkmode"]: + if options[0]["runtime"]["darkmode"]: args.extend(["-platform", "windows:darkmode=2"]) args.extend(sys.argv) app = Application(args) @@ -97,7 +96,7 @@ def open_tagger_window( raise SystemExit(1) # needed to catch initial open file events (macOS) - app.openFileRequest.connect(lambda x: options["runtime"]["files"].append(x.toLocalFile())) + app.openFileRequest.connect(lambda x: options[0]["runtime"]["files"].append(x.toLocalFile())) if platform.system() == "Darwin": # Set the MacOS dock icon @@ -125,7 +124,7 @@ def open_tagger_window( QtWidgets.QApplication.processEvents() try: - tagger_window = TaggerWindow(options["runtime"]["files"], options, talker_api) + tagger_window = TaggerWindow(options[0]["runtime"]["files"], options, talker_api) tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) tagger_window.show() diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 7b30fb9..368213f 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -20,13 +20,13 @@ import logging import sys from typing import Any, Callable +import settngs from typing_extensions import NotRequired, TypedDict from comicapi import utils from comicapi.comicarchive import ComicArchive from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString -from comictaggerlib import settings from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException from comictaggerlib.imagehasher import ImageHasher from comictaggerlib.resulttypes import IssueResult @@ -72,7 +72,7 @@ class IssueIdentifier: result_one_good_match = 4 result_multiple_good_matches = 5 - def __init__(self, comic_archive: ComicArchive, options: settings.OptionValues, talker_api: ComicTalker) -> None: + def __init__(self, comic_archive: ComicArchive, options: settngs.ConfigValues, talker_api: ComicTalker) -> None: self.options = options self.talker_api = talker_api self.comic_archive: ComicArchive = comic_archive diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 0c5694d..ca03b26 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -17,10 +17,10 @@ from __future__ import annotations import logging +import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic from comicapi.issuestring import IssueString -from comictaggerlib import settings from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size @@ -44,7 +44,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): def __init__( self, parent: QtWidgets.QWidget, - options: settings.OptionValues, + options: settngs.ConfigValues, talker_api: ComicTalker, series_id: int, issue_number: str, diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 01a62c3..509656b 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -24,9 +24,11 @@ import signal import sys from typing import Any +import settngs + import comictalker.comictalkerapi as ct_api from comicapi import utils -from comictaggerlib import cli, settings +from comictaggerlib import cli, ctoptions from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging from comictalker.talkerbase import TalkerError @@ -63,8 +65,8 @@ class App: """docstring for App""" def __init__(self) -> None: - self.options: dict[str, dict[str, Any]] = {} - self.initial_arg_parser = settings.initial_cmd_line_parser() + self.options = settngs.Config({}, {}) + self.initial_arg_parser = ctoptions.initial_cmd_line_parser() def run(self) -> None: opts = self.initialize() @@ -81,29 +83,33 @@ class App: return opts def register_options(self) -> None: - self.manager = settings.Manager( + self.manager = settngs.Manager( """A utility for reading and writing metadata to comic archives.\n\n\nIf no options are given, %(prog)s will run in windowed mode.""", "For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki", ) - settings.register_commandline(self.manager) - settings.register_settings(self.manager) + ctoptions.register_commandline(self.manager) + ctoptions.register_settings(self.manager) - def parse_options(self, config_paths: settings.ComicTaggerPaths) -> None: - options = self.manager.parse_options(config_paths.user_config_dir / "settings.json") - self.options = settings.validate_commandline_options(options, self.manager) - self.options = settings.validate_settings(options, self.manager) + def parse_options(self, config_paths: ctoptions.ComicTaggerPaths) -> None: + config, success = self.manager.parse_config(config_paths.user_config_dir / "settings.json") + options, definitions = config + if not success: + raise SystemExit(99) + options = ctoptions.validate_commandline_options(options, self.manager) + options = ctoptions.validate_settings(options, self.manager) + self.options = settngs.Config(options, definitions) def initialize_dirs(self) -> None: - self.options["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True) - self.options["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True) - self.options["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True) - self.options["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True) - self.options["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True) - logger.debug("user_data_dir: %s", self.options["runtime"]["config"].user_data_dir) - logger.debug("user_config_dir: %s", self.options["runtime"]["config"].user_config_dir) - logger.debug("user_cache_dir: %s", self.options["runtime"]["config"].user_cache_dir) - logger.debug("user_state_dir: %s", self.options["runtime"]["config"].user_state_dir) - logger.debug("user_log_dir: %s", self.options["runtime"]["config"].user_log_dir) + self.options[0]["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True) + self.options[0]["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True) + self.options[0]["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True) + self.options[0]["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True) + self.options[0]["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True) + logger.debug("user_data_dir: %s", self.options[0]["runtime"]["config"].user_data_dir) + logger.debug("user_config_dir: %s", self.options[0]["runtime"]["config"].user_config_dir) + logger.debug("user_cache_dir: %s", self.options[0]["runtime"]["config"].user_cache_dir) + logger.debug("user_state_dir: %s", self.options[0]["runtime"]["config"].user_state_dir) + logger.debug("user_log_dir: %s", self.options[0]["runtime"]["config"].user_log_dir) def ctmain(self) -> None: assert self.options is not None @@ -111,10 +117,11 @@ class App: # manage the CV API key # None comparison is used so that the empty string can unset the value - if self.options["comicvine"]["cv_api_key"] is not None or self.options["comicvine"]["cv_url"] is not None: - self.manager.save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json") - logger.debug(pprint.pformat(self.options)) - if self.options["commands"]["only_set_cv_key"]: + if self.options[0]["comicvine"]["cv_api_key"] is not None or self.options[0]["comicvine"]["cv_url"] is not None: + settings_path = self.options[0]["runtime"]["config"].user_config_dir / "settings.json" + self.manager.save_file(self.options[0], settings_path) + logger.debug(pprint.pformat(self.options[0])) + if self.options[0]["commands"]["only_set_cv_key"]: print("Key set") # noqa: T201 return @@ -132,33 +139,33 @@ class App: logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"]) utils.load_publishers() - update_publishers(self.options) + update_publishers(self.options[0]) - if not qt_available and not self.options["runtime"]["no_gui"]: - self.options["runtime"]["no_gui"] = True + if not qt_available and not self.options[0]["runtime"]["no_gui"]: + self.options[0]["runtime"]["no_gui"] = True logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.") gui_exception = None try: talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg] version=version, - cache_folder=self.options["runtime"]["config"].user_cache_dir, - series_match_thresh=self.options["comicvine"]["series_match_search_thresh"], - remove_html_tables=self.options["comicvine"]["remove_html_tables"], - use_series_start_as_volume=self.options["comicvine"]["use_series_start_as_volume"], - wait_on_ratelimit=self.options["autotag"]["wait_and_retry_on_rate_limit"], - api_url=self.options["comicvine"]["cv_url"], - api_key=self.options["comicvine"]["cv_api_key"], + cache_folder=self.options[0]["runtime"]["config"].user_cache_dir, + series_match_thresh=self.options[0]["comicvine"]["series_match_search_thresh"], + remove_html_tables=self.options[0]["comicvine"]["remove_html_tables"], + use_series_start_as_volume=self.options[0]["comicvine"]["use_series_start_as_volume"], + wait_on_ratelimit=self.options[0]["autotag"]["wait_and_retry_on_rate_limit"], + api_url=self.options[0]["comicvine"]["cv_url"], + api_key=self.options[0]["comicvine"]["cv_api_key"], ) except TalkerError as e: logger.exception("Unable to load talker") gui_exception = e - if self.options["runtime"]["no_gui"]: + if self.options[0]["runtime"]["no_gui"]: raise SystemExit(1) - if self.options["runtime"]["no_gui"]: + if self.options[0]["runtime"]["no_gui"]: try: - cli.CLI(self.options, talker_api).run() + cli.CLI(self.options[0], talker_api).run() except Exception: logger.exception("CLI mode failed") else: diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index 588fd2c..db93736 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -18,10 +18,10 @@ from __future__ import annotations import logging import os +import settngs from PyQt5 import QtCore, QtWidgets, uic from comicapi.comicarchive import ComicArchive -from comictaggerlib import settings from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.resulttypes import IssueResult from comictaggerlib.ui import ui_path @@ -39,7 +39,7 @@ class MatchSelectionWindow(QtWidgets.QDialog): parent: QtWidgets.QWidget, matches: list[IssueResult], comic_archive: ComicArchive, - options: settings.OptionValues, + options: settngs.Values, talker_api: ComicTalker, ) -> None: super().__init__(parent) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index a3f3d78..5006769 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -17,12 +17,12 @@ from __future__ import annotations import logging +import settngs from PyQt5 import QtCore, QtWidgets, uic from comicapi import utils from comicapi.comicarchive import ComicArchive, MetaDataStyle from comicapi.genericmetadata import GenericMetadata -from comictaggerlib import settings from comictaggerlib.filerenamer import FileRenamer, get_rename_dir from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path @@ -38,7 +38,7 @@ class RenameWindow(QtWidgets.QDialog): parent: QtWidgets.QWidget, comic_archive_list: list[ComicArchive], data_style: int, - options: settings.OptionValues, + options: settngs.Config, talker_api: ComicTalker, ) -> None: super().__init__(parent) @@ -61,19 +61,19 @@ class RenameWindow(QtWidgets.QDialog): self.rename_list: list[str] = [] self.btnSettings.clicked.connect(self.modify_settings) - platform = "universal" if self.options["filename"]["rename_strict"] else "auto" - self.renamer = FileRenamer(None, platform=platform, replacements=self.options["rename"]["replacements"]) + platform = "universal" if self.options[0]["filename"]["rename_strict"] else "auto" + self.renamer = FileRenamer(None, platform=platform, replacements=self.options[0]["rename"]["replacements"]) self.do_preview() def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str: - self.renamer.set_template(self.options["filename"]["rename_template"]) - self.renamer.set_issue_zero_padding(self.options["filename"]["rename_issue_number_padding"]) - self.renamer.set_smart_cleanup(self.options["filename"]["rename_use_smart_string_cleanup"]) - self.renamer.replacements = self.options["rename"]["replacements"] + self.renamer.set_template(self.options[0]["filename"]["rename_template"]) + self.renamer.set_issue_zero_padding(self.options[0]["filename"]["rename_issue_number_padding"]) + self.renamer.set_smart_cleanup(self.options[0]["filename"]["rename_use_smart_string_cleanup"]) + self.renamer.replacements = self.options[0]["rename"]["replacements"] new_ext = ca.path.suffix # default - if self.options["filename"]["rename_set_extension_based_on_archive"]: + if self.options[0]["filename"]["rename_set_extension_based_on_archive"]: if ca.is_sevenzip(): new_ext = ".cb7" elif ca.is_zip(): @@ -85,13 +85,13 @@ class RenameWindow(QtWidgets.QDialog): md = ca.read_metadata(self.data_style) if md.is_empty: md = ca.metadata_from_filename( - self.options["filename"]["complicated_parser"], - self.options["filename"]["remove_c2c"], - self.options["filename"]["remove_fcbd"], - self.options["filename"]["remove_publisher"], + self.options[0]["filename"]["complicated_parser"], + self.options[0]["filename"]["remove_c2c"], + self.options[0]["filename"]["remove_fcbd"], + self.options[0]["filename"]["remove_publisher"], ) self.renamer.set_metadata(md) - self.renamer.move = self.options["filename"]["rename_move_to_dir"] + self.renamer.move = self.options[0]["filename"]["rename_move_to_dir"] return new_ext def do_preview(self) -> None: @@ -104,7 +104,7 @@ class RenameWindow(QtWidgets.QDialog): try: new_name = self.renamer.determine_name(new_ext) except ValueError as e: - logger.exception("Invalid format string: %s", self.options["filename"]["rename_template"]) + logger.exception("Invalid format string: %s", self.options[0]["filename"]["rename_template"]) QtWidgets.QMessageBox.critical( self, "Invalid format string!", @@ -119,7 +119,7 @@ class RenameWindow(QtWidgets.QDialog): except Exception as e: logger.exception( "Formatter failure: %s metadata: %s", - self.options["filename"]["rename_template"], + self.options[0]["filename"]["rename_template"], self.renamer.metadata, ) QtWidgets.QMessageBox.critical( @@ -197,7 +197,9 @@ class RenameWindow(QtWidgets.QDialog): folder = get_rename_dir( comic[0], - self.options["filename"]["rename_dir"] if self.options["filename"]["rename_move_to_dir"] else None, + self.options[0]["filename"]["rename_dir"] + if self.options[0]["filename"]["rename_move_to_dir"] + else None, ) full_path = folder / comic[1] diff --git a/comictaggerlib/settings/__init__.py b/comictaggerlib/settings/__init__.py deleted file mode 100644 index c4c4a90..0000000 --- a/comictaggerlib/settings/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -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, defaults, save_file -from comictaggerlib.settings.types import ComicTaggerPaths - -__all__ = [ - "initial_cmd_line_parser", - "register_commandline", - "register_settings", - "validate_commandline_options", - "validate_settings", - "Manager", - "ComicTaggerPaths", - "OptionValues", - "OptionDefinitions", - "save_file", - "defaults", -] diff --git a/comictaggerlib/settings/manager.py b/comictaggerlib/settings/manager.py deleted file mode 100644 index fa34606..0000000 --- a/comictaggerlib/settings/manager.py +++ /dev/null @@ -1,401 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import logging -import pathlib -from collections import defaultdict -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union - -logger = logging.getLogger(__name__) - - -class Setting: - def __init__( - self, - # From argparse - *names: str, - action: type[argparse.Action] | None = None, - nargs: str | int | None = None, - const: str | None = None, - default: str | None = None, - type: Callable[..., Any] | None = None, # noqa: A002 - choices: Sequence[Any] | None = None, - required: bool | None = None, - help: str | None = None, # noqa: A002 - metavar: str | None = None, - dest: str | None = None, - # ComicTagger - cmdline: bool = True, - file: bool = True, - group: str = "", - exclusive: bool = False, - ): - if not names: - raise ValueError("names must be specified") - # We prefix the destination name used by argparse so that there are no conflicts - # Argument names will still cause an exception if there is a conflict e.g. if '-f' is defined twice - self.internal_name, dest, flag = self.get_dest(group, names, dest) - args: Sequence[str] = names - - # We then also set the metavar so that '--config' in the group runtime shows as 'CONFIG' instead of 'RUNTIME_CONFIG' - if not metavar and action not in ("store_true", "store_false", "count"): - metavar = dest.upper() - - # If we are not a flag, no '--' or '-' in front - # we prefix the first name with the group as argparse sets dest to args[0] - # I believe internal name may be able to be used here - if not flag: - args = tuple((f"{group}_{names[0]}".lstrip("_"), *names[1:])) - - self.action = action - self.nargs = nargs - self.const = const - self.default = default - self.type = type - self.choices = choices - self.required = required - self.help = help - self.metavar = metavar - self.dest = dest - self.cmdline = cmdline - self.file = file - self.argparse_args = args - self.group = group - self.exclusive = exclusive - - self.argparse_kwargs = { - "action": action, - "nargs": nargs, - "const": const, - "default": default, - "type": type, - "choices": choices, - "required": required, - "help": help, - "metavar": metavar, - "dest": self.internal_name if flag else None, - } - - def __str__(self) -> str: - return f"Setting({self.argparse_args}, type={self.type}, file={self.file}, cmdline={self.cmdline}, kwargs={self.argparse_kwargs})" - - def __repr__(self) -> str: - return self.__str__() - - def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, bool]: - dest_name = None - flag = False - - for n in names: - if n.startswith("--"): - flag = True - dest_name = n.lstrip("-").replace("-", "_") - break - if n.startswith("-"): - flag = True - - if dest_name is None: - dest_name = names[0] - if dest: - dest_name = dest - if dest_name is None: - raise Exception("Something failed, try again") - - internal_name = f"{prefix}_{dest_name}".lstrip("_") - return internal_name, dest_name, flag - - def filter_argparse_kwargs(self) -> dict[str, Any]: - return {k: v for k, v in self.argparse_kwargs.items() if v is not None} - - def to_argparse(self) -> tuple[Sequence[str], dict[str, Any]]: - return self.argparse_args, self.filter_argparse_kwargs() - - -OptionValues = dict[str, dict[str, Any]] -OptionDefinitions = dict[str, dict[str, Setting]] -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: - """docstring for SettingManager""" - - def __init__( - self, description: str | None = None, epilog: str | None = None, definitions: OptionDefinitions | None = None - ): - # This one is never used, it just makes MyPy happy - self.argparser = argparse.ArgumentParser(description=description, epilog=epilog) - self.description = description - self.epilog = epilog - - self.definitions: OptionDefinitions = defaultdict(lambda: dict()) - if definitions: - self.definitions = definitions - - self.exclusive_group = False - self.current_group_name = "" - - 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.definitions[self.current_group_name][setting.dest] = setting - - def create_argparser(self) -> None: - """Creates an :class:`argparse.ArgumentParser` from all cmdline settings""" - groups: dict[str, ArgParser] = {} - self.argparser = argparse.ArgumentParser( - description=self.description, - epilog=self.epilog, - formatter_class=argparse.RawTextHelpFormatter, - ) - 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() - current_group: ArgParser = self.argparser - if setting.group: - if setting.group not in groups: - if setting.exclusive: - groups[setting.group] = self.argparser.add_argument_group( - setting.group, - ).add_mutually_exclusive_group() - else: - groups[setting.group] = self.argparser.add_argument_group(setting.group) - - # hard coded exception for files - if not (setting.group == "runtime" and setting.nargs == "*"): - current_group = groups[setting.group] - 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) - self.current_group_name = "" - self.exclusive_group = False - - def exit(self, *args: Any, **kwargs: Any) -> NoReturn: - """See :class:`~argparse.ArgumentParser`""" - self.argparser.exit(*args, **kwargs) - raise SystemExit(99) - - def defaults(self) -> OptionValues: - return defaults(self.definitions) - - 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, - raw_options: OptionValues | argparse.Namespace, - file: bool = False, - cmdline: bool = False, - defaults: bool = True, - raw_options_2: OptionValues | argparse.Namespace | None = None, - ) -> OptionValues: - return normalize_options( - raw_options=raw_options, - definitions=self.definitions, - file=file, - cmdline=cmdline, - defaults=defaults, - raw_options_2=raw_options_2, - ) - - def get_namespace(self, options: OptionValues, defaults: bool = True) -> argparse.Namespace: - return get_namespace(options=options, definitions=self.definitions, defaults=defaults) - - 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.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 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, - ) - cli_options = self.parse_args(args, self.get_namespace(file_options, defaults=False)) - - 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 cb164c4..9e5fa3f 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -22,11 +22,11 @@ import pathlib import platform from typing import Any +import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic from comicapi import utils from comicapi.genericmetadata import md_test -from comictaggerlib import settings from comictaggerlib.ctversion import version from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements from comictaggerlib.imagefetcher import ImageFetcher @@ -130,7 +130,7 @@ Spider-Geddon #1 - New Players; Check In class SettingsWindow(QtWidgets.QDialog): - def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, talker_api: ComicTalker) -> None: + def __init__(self, parent: QtWidgets.QWidget, options: settngs.Config, talker_api: ComicTalker) -> None: super().__init__(parent) uic.loadUi(ui_path / "settingswindow.ui", self) @@ -269,52 +269,54 @@ class SettingsWindow(QtWidgets.QDialog): def settings_to_form(self) -> None: # Copy values from settings to form - self.leRarExePath.setText(self.options["general"]["rar_exe_path"]) - self.sbNameMatchIdentifyThresh.setValue(self.options["identifier"]["series_match_identify_thresh"]) - self.sbNameMatchSearchThresh.setValue(self.options["comicvine"]["series_match_search_thresh"]) - self.tePublisherFilter.setPlainText("\n".join(self.options["identifier"]["publisher_filter"])) + self.leRarExePath.setText(self.options[0]["general"]["rar_exe_path"]) + self.sbNameMatchIdentifyThresh.setValue(self.options[0]["identifier"]["series_match_identify_thresh"]) + self.sbNameMatchSearchThresh.setValue(self.options[0]["comicvine"]["series_match_search_thresh"]) + self.tePublisherFilter.setPlainText("\n".join(self.options[0]["identifier"]["publisher_filter"])) - self.cbxCheckForNewVersion.setChecked(self.options["general"]["check_for_new_version"]) + self.cbxCheckForNewVersion.setChecked(self.options[0]["general"]["check_for_new_version"]) - self.cbxComplicatedParser.setChecked(self.options["filename"]["complicated_parser"]) - self.cbxRemoveC2C.setChecked(self.options["filename"]["remove_c2c"]) - self.cbxRemoveFCBD.setChecked(self.options["filename"]["remove_fcbd"]) - self.cbxRemovePublisher.setChecked(self.options["filename"]["remove_publisher"]) + self.cbxComplicatedParser.setChecked(self.options[0]["filename"]["complicated_parser"]) + self.cbxRemoveC2C.setChecked(self.options[0]["filename"]["remove_c2c"]) + self.cbxRemoveFCBD.setChecked(self.options[0]["filename"]["remove_fcbd"]) + self.cbxRemovePublisher.setChecked(self.options[0]["filename"]["remove_publisher"]) self.switch_parser() - self.cbxUseSeriesStartAsVolume.setChecked(self.options["comicvine"]["use_series_start_as_volume"]) - self.cbxClearFormBeforePopulating.setChecked(self.options["comicvine"]["clear_form_before_populating_from_cv"]) - self.cbxRemoveHtmlTables.setChecked(self.options["comicvine"]["remove_html_tables"]) + self.cbxUseSeriesStartAsVolume.setChecked(self.options[0]["comicvine"]["use_series_start_as_volume"]) + self.cbxClearFormBeforePopulating.setChecked( + self.options[0]["comicvine"]["clear_form_before_populating_from_cv"] + ) + self.cbxRemoveHtmlTables.setChecked(self.options[0]["comicvine"]["remove_html_tables"]) - self.cbxUseFilter.setChecked(self.options["comicvine"]["always_use_publisher_filter"]) - self.cbxSortByYear.setChecked(self.options["comicvine"]["sort_series_by_year"]) - self.cbxExactMatches.setChecked(self.options["comicvine"]["exact_series_matches_first"]) + self.cbxUseFilter.setChecked(self.options[0]["comicvine"]["always_use_publisher_filter"]) + self.cbxSortByYear.setChecked(self.options[0]["comicvine"]["sort_series_by_year"]) + self.cbxExactMatches.setChecked(self.options[0]["comicvine"]["exact_series_matches_first"]) - self.leKey.setText(self.options["comicvine"]["cv_api_key"]) - self.leURL.setText(self.options["comicvine"]["cv_url"]) + self.leKey.setText(self.options[0]["comicvine"]["cv_api_key"]) + self.leURL.setText(self.options[0]["comicvine"]["cv_url"]) - self.cbxAssumeLoneCreditIsPrimary.setChecked(self.options["cbl"]["assume_lone_credit_is_primary"]) - self.cbxCopyCharactersToTags.setChecked(self.options["cbl"]["copy_characters_to_tags"]) - self.cbxCopyTeamsToTags.setChecked(self.options["cbl"]["copy_teams_to_tags"]) - self.cbxCopyLocationsToTags.setChecked(self.options["cbl"]["copy_locations_to_tags"]) - self.cbxCopyStoryArcsToTags.setChecked(self.options["cbl"]["copy_storyarcs_to_tags"]) - self.cbxCopyNotesToComments.setChecked(self.options["cbl"]["copy_notes_to_comments"]) - self.cbxCopyWebLinkToComments.setChecked(self.options["cbl"]["copy_weblink_to_comments"]) - self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options["cbl"]["apply_cbl_transform_on_cv_import"]) + self.cbxAssumeLoneCreditIsPrimary.setChecked(self.options[0]["cbl"]["assume_lone_credit_is_primary"]) + self.cbxCopyCharactersToTags.setChecked(self.options[0]["cbl"]["copy_characters_to_tags"]) + self.cbxCopyTeamsToTags.setChecked(self.options[0]["cbl"]["copy_teams_to_tags"]) + self.cbxCopyLocationsToTags.setChecked(self.options[0]["cbl"]["copy_locations_to_tags"]) + self.cbxCopyStoryArcsToTags.setChecked(self.options[0]["cbl"]["copy_storyarcs_to_tags"]) + self.cbxCopyNotesToComments.setChecked(self.options[0]["cbl"]["copy_notes_to_comments"]) + self.cbxCopyWebLinkToComments.setChecked(self.options[0]["cbl"]["copy_weblink_to_comments"]) + self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options[0]["cbl"]["apply_cbl_transform_on_cv_import"]) self.cbxApplyCBLTransformOnBatchOperation.setChecked( - self.options["cbl"]["apply_cbl_transform_on_bulk_operation"] + self.options[0]["cbl"]["apply_cbl_transform_on_bulk_operation"] ) - self.leRenameTemplate.setText(self.options["rename"]["template"]) - self.leIssueNumPadding.setText(str(self.options["rename"]["issue_number_padding"])) - self.cbxSmartCleanup.setChecked(self.options["rename"]["use_smart_string_cleanup"]) - self.cbxChangeExtension.setChecked(self.options["rename"]["set_extension_based_on_archive"]) - self.cbxMoveFiles.setChecked(self.options["rename"]["move_to_dir"]) - self.leDirectory.setText(self.options["rename"]["dir"]) - self.cbxRenameStrict.setChecked(self.options["rename"]["strict"]) + self.leRenameTemplate.setText(self.options[0]["rename"]["template"]) + self.leIssueNumPadding.setText(str(self.options[0]["rename"]["issue_number_padding"])) + self.cbxSmartCleanup.setChecked(self.options[0]["rename"]["use_smart_string_cleanup"]) + self.cbxChangeExtension.setChecked(self.options[0]["rename"]["set_extension_based_on_archive"]) + self.cbxMoveFiles.setChecked(self.options[0]["rename"]["move_to_dir"]) + self.leDirectory.setText(self.options[0]["rename"]["dir"]) + self.cbxRenameStrict.setChecked(self.options[0]["rename"]["strict"]) for table, replacments in zip( - (self.twLiteralReplacements, self.twValueReplacements), self.options["rename"]["replacements"] + (self.twLiteralReplacements, self.twValueReplacements), self.options[0]["rename"]["replacements"] ): table.clearContents() for i in reversed(range(table.rowCount())): @@ -349,7 +351,7 @@ class SettingsWindow(QtWidgets.QDialog): self.rename_test() if self.rename_error is not None: if isinstance(self.rename_error, ValueError): - logger.exception("Invalid format string: %s", self.options["rename"]["template"]) + logger.exception("Invalid format string: %s", self.options[0]["rename"]["template"]) QtWidgets.QMessageBox.critical( self, "Invalid format string!", @@ -363,7 +365,7 @@ class SettingsWindow(QtWidgets.QDialog): return else: logger.exception( - "Formatter failure: %s metadata: %s", self.options["rename"]["template"], self.renamer.metadata + "Formatter failure: %s metadata: %s", self.options[0]["rename"]["template"], self.renamer.metadata ) QtWidgets.QMessageBox.critical( self, @@ -376,72 +378,70 @@ class SettingsWindow(QtWidgets.QDialog): ) # Copy values from form to settings and save - self.options["general"]["rar_exe_path"] = str(self.leRarExePath.text()) + self.options[0]["general"]["rar_exe_path"] = str(self.leRarExePath.text()) # make sure rar program is now in the path for the rar class - if self.options["general"]["rar_exe_path"]: - utils.add_to_path(os.path.dirname(self.options["general"]["rar_exe_path"])) + if self.options[0]["general"]["rar_exe_path"]: + utils.add_to_path(os.path.dirname(self.options[0]["general"]["rar_exe_path"])) if not str(self.leIssueNumPadding.text()).isdigit(): self.leIssueNumPadding.setText("0") - self.options["general"]["check_for_new_version"] = self.cbxCheckForNewVersion.isChecked() + self.options[0]["general"]["check_for_new_version"] = self.cbxCheckForNewVersion.isChecked() - self.options["identifier"]["series_match_identify_thresh"] = self.sbNameMatchIdentifyThresh.value() - self.options["comicvine"]["series_match_search_thresh"] = self.sbNameMatchSearchThresh.value() - self.options["identifier"]["publisher_filter"] = [ + self.options[0]["identifier"]["series_match_identify_thresh"] = self.sbNameMatchIdentifyThresh.value() + self.options[0]["comicvine"]["series_match_search_thresh"] = self.sbNameMatchSearchThresh.value() + self.options[0]["identifier"]["publisher_filter"] = [ x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip() ] - self.options["filename"]["complicated_parser"] = self.cbxComplicatedParser.isChecked() - self.options["filename"]["remove_c2c"] = self.cbxRemoveC2C.isChecked() - self.options["filename"]["remove_fcbd"] = self.cbxRemoveFCBD.isChecked() - self.options["filename"]["remove_publisher"] = self.cbxRemovePublisher.isChecked() + self.options[0]["filename"]["complicated_parser"] = self.cbxComplicatedParser.isChecked() + self.options[0]["filename"]["remove_c2c"] = self.cbxRemoveC2C.isChecked() + self.options[0]["filename"]["remove_fcbd"] = self.cbxRemoveFCBD.isChecked() + self.options[0]["filename"]["remove_publisher"] = self.cbxRemovePublisher.isChecked() - self.options["comicvine"]["use_series_start_as_volume"] = self.cbxUseSeriesStartAsVolume.isChecked() - self.options["comicvine"][ + self.options[0]["comicvine"]["use_series_start_as_volume"] = self.cbxUseSeriesStartAsVolume.isChecked() + self.options[0]["comicvine"][ "clear_form_before_populating_from_cv" ] = self.cbxClearFormBeforePopulating.isChecked() - self.options["comicvine"]["remove_html_tables"] = self.cbxRemoveHtmlTables.isChecked() + self.options[0]["comicvine"]["remove_html_tables"] = self.cbxRemoveHtmlTables.isChecked() - self.options["comicvine"]["always_use_publisher_filter"] = self.cbxUseFilter.isChecked() - self.options["comicvine"]["sort_series_by_year"] = self.cbxSortByYear.isChecked() - self.options["comicvine"]["exact_series_matches_first"] = self.cbxExactMatches.isChecked() + self.options[0]["comicvine"]["always_use_publisher_filter"] = self.cbxUseFilter.isChecked() + self.options[0]["comicvine"]["sort_series_by_year"] = self.cbxSortByYear.isChecked() + self.options[0]["comicvine"]["exact_series_matches_first"] = self.cbxExactMatches.isChecked() if self.leKey.text().strip(): - self.options["comicvine"]["cv_api_key"] = self.leKey.text().strip() - self.talker_api.api_key = self.options["comicvine"]["cv_api_key"] + self.options[0]["comicvine"]["cv_api_key"] = self.leKey.text().strip() + self.talker_api.api_key = self.options[0]["comicvine"]["cv_api_key"] if self.leURL.text().strip(): - self.options["comicvine"]["cv_url"] = self.leURL.text().strip() - self.talker_api.api_url = self.options["comicvine"]["cv_url"] + self.options[0]["comicvine"]["cv_url"] = self.leURL.text().strip() + self.talker_api.api_url = self.options[0]["comicvine"]["cv_url"] - self.options["cbl"]["assume_lone_credit_is_primary"] = self.cbxAssumeLoneCreditIsPrimary.isChecked() - self.options["cbl"]["copy_characters_to_tags"] = self.cbxCopyCharactersToTags.isChecked() - self.options["cbl"]["copy_teams_to_tags"] = self.cbxCopyTeamsToTags.isChecked() - self.options["cbl"]["copy_locations_to_tags"] = self.cbxCopyLocationsToTags.isChecked() - self.options["cbl"]["copy_storyarcs_to_tags"] = self.cbxCopyStoryArcsToTags.isChecked() - self.options["cbl"]["copy_notes_to_comments"] = self.cbxCopyNotesToComments.isChecked() - self.options["cbl"]["copy_weblink_to_comments"] = self.cbxCopyWebLinkToComments.isChecked() - self.options["cbl"]["apply_cbl_transform_on_cv_import"] = self.cbxApplyCBLTransformOnCVIMport.isChecked() - self.options["cbl"][ + self.options[0]["cbl"]["assume_lone_credit_is_primary"] = self.cbxAssumeLoneCreditIsPrimary.isChecked() + self.options[0]["cbl"]["copy_characters_to_tags"] = self.cbxCopyCharactersToTags.isChecked() + self.options[0]["cbl"]["copy_teams_to_tags"] = self.cbxCopyTeamsToTags.isChecked() + self.options[0]["cbl"]["copy_locations_to_tags"] = self.cbxCopyLocationsToTags.isChecked() + self.options[0]["cbl"]["copy_storyarcs_to_tags"] = self.cbxCopyStoryArcsToTags.isChecked() + self.options[0]["cbl"]["copy_notes_to_comments"] = self.cbxCopyNotesToComments.isChecked() + self.options[0]["cbl"]["copy_weblink_to_comments"] = self.cbxCopyWebLinkToComments.isChecked() + self.options[0]["cbl"]["apply_cbl_transform_on_cv_import"] = self.cbxApplyCBLTransformOnCVIMport.isChecked() + self.options[0]["cbl"][ "apply_cbl_transform_on_bulk_operation" ] = self.cbxApplyCBLTransformOnBatchOperation.isChecked() - self.options["rename"]["template"] = str(self.leRenameTemplate.text()) - self.options["rename"]["issue_number_padding"] = int(self.leIssueNumPadding.text()) - self.options["rename"]["use_smart_string_cleanup"] = self.cbxSmartCleanup.isChecked() - self.options["rename"]["set_extension_based_on_archive"] = self.cbxChangeExtension.isChecked() - self.options["rename"]["move_to_dir"] = self.cbxMoveFiles.isChecked() - self.options["rename"]["dir"] = self.leDirectory.text() + self.options[0]["rename"]["template"] = str(self.leRenameTemplate.text()) + self.options[0]["rename"]["issue_number_padding"] = int(self.leIssueNumPadding.text()) + self.options[0]["rename"]["use_smart_string_cleanup"] = self.cbxSmartCleanup.isChecked() + self.options[0]["rename"]["set_extension_based_on_archive"] = self.cbxChangeExtension.isChecked() + self.options[0]["rename"]["move_to_dir"] = self.cbxMoveFiles.isChecked() + self.options[0]["rename"]["dir"] = self.leDirectory.text() - self.options["rename"]["strict"] = self.cbxRenameStrict.isChecked() - self.options["rename"]["replacements"] = self.get_replacemnts() + self.options[0]["rename"]["strict"] = self.cbxRenameStrict.isChecked() + self.options[0]["rename"]["replacements"] = self.get_replacemnts() - settings.save_file( - self.options, - self.options["definitions"], - self.options["runtime"]["config"].user_config_dir / "settings.json", + settngs.save_file( + self.options[0], self.options[1], self.options[0]["runtime"]["config"].user_config_dir / "settings.json" ) self.parent().options = self.options QtWidgets.QDialog.accept(self) @@ -450,8 +450,8 @@ class SettingsWindow(QtWidgets.QDialog): self.select_file(self.leRarExePath, "RAR") def clear_cache(self) -> None: - ImageFetcher(self.options["runtime"]["config"].cache_folder).clear_cache() - ComicCacher(self.options["runtime"]["config"].cache_folder, version).clear_cache() + ImageFetcher(self.options[0]["runtime"]["config"].cache_folder).clear_cache() + ComicCacher(self.options[0]["runtime"]["config"].cache_folder, version).clear_cache() QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.") def test_api_key(self) -> None: @@ -461,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.defaults(self.options["definitions"]) + self.options = settngs.Config(settngs.defaults(self.options[1]), self.options[1]) 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 3ec4f5a..e8c7c61 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -31,6 +31,7 @@ from typing import Any, Callable from urllib.parse import urlparse import natsort +import settngs from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic from comicapi import utils @@ -39,7 +40,7 @@ from comicapi.comicinfoxml import ComicInfoXml from comicapi.filenameparser import FileNameParser from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString -from comictaggerlib import ctversion, settings +from comictaggerlib import ctversion from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger from comictaggerlib.autotagmatchwindow import AutoTagMatchWindow from comictaggerlib.autotagprogresswindow import AutoTagProgressWindow @@ -78,7 +79,7 @@ class TaggerWindow(QtWidgets.QMainWindow): def __init__( self, file_list: list[str], - options: settings.OptionValues, + options: settngs.Config, talker_api: ComicTalker, parent: QtWidgets.QWidget | None = None, ) -> None: @@ -87,17 +88,17 @@ class TaggerWindow(QtWidgets.QMainWindow): uic.loadUi(ui_path / "taggerwindow.ui", self) self.options = options if not options: - self.options = {} + self.options = ({}, {}) self.talker_api = talker_api self.log_window = self.setup_logger() # prevent multiple instances socket = QtNetwork.QLocalSocket(self) - socket.connectToServer(options["internal"]["install_id"]) + socket.connectToServer(options[0]["internal"]["install_id"]) alive = socket.waitForConnected(3000) if alive: logger.setLevel(logging.INFO) - logger.info("Another application with key [%s] is already running", options["internal"]["install_id"]) + logger.info("Another application with key [%s] is already running", options[0]["internal"]["install_id"]) # send file list to other instance if file_list: socket.write(pickle.dumps(file_list)) @@ -109,15 +110,15 @@ class TaggerWindow(QtWidgets.QMainWindow): # listen on a socket to prevent multiple instances self.socketServer = QtNetwork.QLocalServer(self) self.socketServer.newConnection.connect(self.on_incoming_socket_connection) - ok = self.socketServer.listen(options["internal"]["install_id"]) + ok = self.socketServer.listen(options[0]["internal"]["install_id"]) if not ok: if self.socketServer.serverError() == QtNetwork.QAbstractSocket.SocketError.AddressInUseError: - self.socketServer.removeServer(options["internal"]["install_id"]) - ok = self.socketServer.listen(options["internal"]["install_id"]) + self.socketServer.removeServer(options[0]["internal"]["install_id"]) + ok = self.socketServer.listen(options[0]["internal"]["install_id"]) if not ok: logger.error( "Cannot start local socket with key [%s]. Reason: %s", - options["internal"]["install_id"], + options[0]["internal"]["install_id"], self.socketServer.errorString(), ) sys.exit() @@ -131,15 +132,15 @@ class TaggerWindow(QtWidgets.QMainWindow): grid_layout = QtWidgets.QGridLayout(self.tabPages) grid_layout.addWidget(self.page_list_editor) - self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.options, self.dirty_flag_verification) + self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.options[0], self.dirty_flag_verification) grid_layout = QtWidgets.QGridLayout(self.widgetListHolder) grid_layout.addWidget(self.fileSelectionList) self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed) self.fileSelectionList.listCleared.connect(self.file_list_cleared) self.fileSelectionList.set_sorting( - self.options["internal"]["last_filelist_sorted_column"], - QtCore.Qt.SortOrder(self.options["internal"]["last_filelist_sorted_order"]), + self.options[0]["internal"]["last_filelist_sorted_column"], + QtCore.Qt.SortOrder(self.options[0]["internal"]["last_filelist_sorted_order"]), ) # we can't specify relative font sizes in the UI designer, so @@ -157,13 +158,13 @@ class TaggerWindow(QtWidgets.QMainWindow): self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) - if options["runtime"]["type"] and isinstance(options["runtime"]["type"][0], int): + if options[0]["runtime"]["type"] and isinstance(options[0]["runtime"]["type"][0], int): # respect the command line option tag type - options["internal"]["last_selected_save_data_style"] = options["runtime"]["type"][0] - options["internal"]["last_selected_load_data_style"] = options["runtime"]["type"][0] + options[0]["internal"]["last_selected_save_data_style"] = options[0]["runtime"]["type"][0] + options[0]["internal"]["last_selected_load_data_style"] = options[0]["runtime"]["type"][0] - self.save_data_style = options["internal"]["last_selected_save_data_style"] - self.load_data_style = options["internal"]["last_selected_load_data_style"] + self.save_data_style = options[0]["internal"]["last_selected_save_data_style"] + self.load_data_style = options[0]["internal"]["last_selected_load_data_style"] self.setAcceptDrops(True) self.config_menus() @@ -228,9 +229,12 @@ class TaggerWindow(QtWidgets.QMainWindow): self.show() self.set_app_position() - if self.options["internal"]["last_form_side_width"] != -1: + if self.options[0]["internal"]["last_form_side_width"] != -1: self.splitter.setSizes( - [self.options["internal"]["last_form_side_width"], self.options["internal"]["last_list_side_width"]] + [ + self.options[0]["internal"]["last_form_side_width"], + self.options[0]["internal"]["last_list_side_width"], + ] ) self.raise_() QtCore.QCoreApplication.processEvents() @@ -248,7 +252,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if len(file_list) != 0: self.fileSelectionList.add_path_list(file_list) - if self.options["dialog"]["show_disclaimer"]: + if self.options[0]["dialog"]["show_disclaimer"]: checked = OptionalMessageDialog.msg( self, "Welcome!", @@ -263,9 +267,9 @@ use ComicTagger on local copies of your comics.

Have fun! """, ) - self.options["dialog"]["show_disclaimer"] = not checked + self.options[0]["dialog"]["show_disclaimer"] = not checked - if self.options["general"]["check_for_new_version"]: + if self.options[0]["general"]["check_for_new_version"]: self.check_latest_version_online() def open_file_event(self, url: QtCore.QUrl) -> None: @@ -278,7 +282,7 @@ Have fun! def setup_logger(self) -> ApplicationLogWindow: try: - current_logs = (self.options["runtime"]["config"].user_log_dir / "ComicTagger.log").read_text("utf-8") + current_logs = (self.options[0]["runtime"]["config"].user_log_dir / "ComicTagger.log").read_text("utf-8") except Exception: current_logs = "" root_logger = logging.getLogger() @@ -611,10 +615,10 @@ Have fun! def actual_load_current_archive(self) -> None: if self.metadata.is_empty and self.comic_archive is not None: self.metadata = self.comic_archive.metadata_from_filename( - self.options["filename"]["complicated_parser"], - self.options["filename"]["remove_c2c"], - self.options["filename"]["remove_fcbd"], - self.options["filename"]["remove_publisher"], + self.options[0]["filename"]["complicated_parser"], + self.options[0]["filename"]["remove_c2c"], + self.options[0]["filename"]["remove_fcbd"], + self.options[0]["filename"]["remove_publisher"], ) if len(self.metadata.pages) == 0 and self.comic_archive is not None: self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages()) @@ -982,10 +986,10 @@ Have fun! # copy the form onto metadata object self.form_to_metadata() new_metadata = self.comic_archive.metadata_from_filename( - self.options["filename"]["complicated_parser"], - self.options["filename"]["remove_c2c"], - self.options["filename"]["remove_fcbd"], - self.options["filename"]["remove_publisher"], + self.options[0]["filename"]["complicated_parser"], + self.options[0]["filename"]["remove_c2c"], + self.options[0]["filename"]["remove_fcbd"], + self.options[0]["filename"]["remove_publisher"], split_words, ) if new_metadata is not None: @@ -1006,8 +1010,8 @@ Have fun! else: dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles) - if self.options["internal"]["last_opened_folder"] is not None: - dialog.setDirectory(self.options["internal"]["last_opened_folder"]) + if self.options[0]["internal"]["last_opened_folder"] is not None: + dialog.setDirectory(self.options[0]["internal"]["last_opened_folder"]) if not folder_mode: archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)" @@ -1057,7 +1061,7 @@ Have fun! issue_count, cover_index_list, self.comic_archive, - self.options, + self.options[0], self.talker_api, autoselect, literal, @@ -1089,10 +1093,10 @@ Have fun! else: QtWidgets.QApplication.restoreOverrideCursor() if new_metadata is not None: - if self.options["cbl"]["apply_cbl_transform_on_cv_import"]: - new_metadata = CBLTransformer(new_metadata, self.options).apply() + if self.options[0]["cbl"]["apply_cbl_transform_on_cv_import"]: + new_metadata = CBLTransformer(new_metadata, self.options[0]).apply() - if self.options["comicvine"]["clear_form_before_populating_from_cv"]: + if self.options[0]["comicvine"]["clear_form_before_populating_from_cv"]: self.clear_form() notes = ( @@ -1147,7 +1151,7 @@ Have fun! "Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?" ): self.load_data_style = self.cbLoadDataStyle.itemData(s) - self.options["internal"]["last_selected_load_data_style"] = self.load_data_style + self.options[0]["internal"]["last_selected_load_data_style"] = self.load_data_style self.update_menus() if self.comic_archive is not None: self.load_archive(self.comic_archive) @@ -1158,7 +1162,7 @@ Have fun! def set_save_data_style(self, s: int) -> None: self.save_data_style = self.cbSaveDataStyle.itemData(s) - self.options["internal"]["last_selected_save_data_style"] = self.save_data_style + self.options[0]["internal"]["last_selected_save_data_style"] = self.save_data_style self.update_style_tweaks() self.update_menus() @@ -1383,10 +1387,13 @@ Have fun! settingswin.result() def set_app_position(self) -> None: - if self.options["internal"]["last_main_window_width"] != 0: - self.move(self.options["internal"]["last_main_window_x"], self.options["internal"]["last_main_window_y"]) + if self.options[0]["internal"]["last_main_window_width"] != 0: + self.move( + self.options[0]["internal"]["last_main_window_x"], self.options[0]["internal"]["last_main_window_y"] + ) self.resize( - self.options["internal"]["last_main_window_width"], self.options["internal"]["last_main_window_height"] + self.options[0]["internal"]["last_main_window_width"], + self.options[0]["internal"]["last_main_window_height"], ) else: screen = QtGui.QGuiApplication.primaryScreen().geometry() @@ -1656,9 +1663,9 @@ Have fun! if ( dest_style == MetaDataStyle.CBI - and self.options["cbl"]["apply_cbl_transform_on_bulk_operation"] + and self.options[0]["cbl"]["apply_cbl_transform_on_bulk_operation"] ): - md = CBLTransformer(md, self.options).apply() + md = CBLTransformer(md, self.options[0]).apply() if not ca.write_metadata(md, dest_style): failed_list.append(ca.path) @@ -1696,8 +1703,8 @@ Have fun! logger.exception("Save aborted.") if not ct_md.is_empty: - if self.options["cbl"]["apply_cbl_transform_on_cv_import"]: - ct_md = CBLTransformer(ct_md, self.options).apply() + if self.options[0]["cbl"]["apply_cbl_transform_on_cv_import"]: + ct_md = CBLTransformer(ct_md, self.options[0]).apply() QtWidgets.QApplication.restoreOverrideCursor() @@ -1716,7 +1723,7 @@ Have fun! self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow ) -> tuple[bool, OnlineMatchResults]: success = False - ii = IssueIdentifier(ca, self.options, self.talker_api) + ii = IssueIdentifier(ca, self.options[0], self.talker_api) # read in metadata, and parse file name if not there try: @@ -1726,10 +1733,10 @@ Have fun! logger.error("Failed to load metadata for %s: %s", ca.path, e) if md.is_empty: md = ca.metadata_from_filename( - self.options["filename"]["complicated_parser"], - self.options["filename"]["remove_c2c"], - self.options["filename"]["remove_fcbd"], - self.options["filename"]["remove_publisher"], + self.options[0]["filename"]["complicated_parser"], + self.options[0]["filename"]["remove_c2c"], + self.options[0]["filename"]["remove_fcbd"], + self.options[0]["filename"]["remove_publisher"], dlg.split_words, ) if dlg.ignore_leading_digits_in_filename and md.series is not None: @@ -1815,7 +1822,7 @@ Have fun! ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.options["comicvine"]["auto_imprint"]: + if self.options[0]["comicvine"]["auto_imprint"]: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): @@ -1844,7 +1851,7 @@ Have fun! atstartdlg = AutoTagStartWindow( self, - self.options, + self.options[0], ( f"You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to." "\n\nPlease choose options below, and select OK to Auto-Tag." @@ -1947,7 +1954,7 @@ Have fun! match_results.multiple_matches, style, self.actual_issue_data_fetch, - self.options, + self.options[0], self.talker_api, ) matchdlg.setModal(True) @@ -1993,20 +2000,18 @@ Have fun! f"Exit {self.appName}", "If you quit now, data in the form will be lost. Are you sure?" ): appsize = self.size() - self.options["internal"]["last_main_window_width"] = appsize.width() - self.options["internal"]["last_main_window_height"] = appsize.height() - self.options["internal"]["last_main_window_x"] = self.x() - self.options["internal"]["last_main_window_y"] = self.y() - self.options["internal"]["last_form_side_width"] = self.splitter.sizes()[0] - self.options["internal"]["last_list_side_width"] = self.splitter.sizes()[1] + self.options[0]["internal"]["last_main_window_width"] = appsize.width() + self.options[0]["internal"]["last_main_window_height"] = appsize.height() + self.options[0]["internal"]["last_main_window_x"] = self.x() + self.options[0]["internal"]["last_main_window_y"] = self.y() + self.options[0]["internal"]["last_form_side_width"] = self.splitter.sizes()[0] + self.options[0]["internal"]["last_list_side_width"] = self.splitter.sizes()[1] ( - self.options["internal"]["last_filelist_sorted_column"], - self.options["internal"]["last_filelist_sorted_order"], + self.options[0]["internal"]["last_filelist_sorted_column"], + self.options[0]["internal"]["last_filelist_sorted_order"], ) = self.fileSelectionList.get_sorting() - settings.save_file( - self.options, - self.options["definitions"], - self.options["runtime"]["config"].user_config_dir / "settings.json", + settngs.save_file( + self.options[0], self.options[1], self.options[0]["runtime"]["config"].user_config_dir / "settings.json" ) event.accept() @@ -2056,7 +2061,7 @@ Have fun! def apply_cbl_transform(self) -> None: self.form_to_metadata() - self.metadata = CBLTransformer(self.metadata, self.options).apply() + self.metadata = CBLTransformer(self.metadata, self.options[0]).apply() self.metadata_to_form() def recalc_page_dimensions(self) -> None: @@ -2101,7 +2106,7 @@ Have fun! QtCore.QTimer.singleShot(1, self.fileSelectionList.revert_selection) return - self.options["internal"]["last_opened_folder"] = os.path.abspath(os.path.split(comic_archive.path)[0]) + self.options[0]["internal"]["last_opened_folder"] = os.path.abspath(os.path.split(comic_archive.path)[0]) self.comic_archive = comic_archive try: self.metadata = self.comic_archive.read_metadata(self.load_data_style) @@ -2134,12 +2139,12 @@ Have fun! version_checker = VersionChecker() self.version_check_complete( version_checker.get_latest_version( - self.options["internal"]["install_id"], self.options["general"]["send_usage_stats"] + self.options[0]["internal"]["install_id"], self.options[0]["general"]["send_usage_stats"] ) ) def version_check_complete(self, new_version: tuple[str, str]) -> None: - if new_version[0] not in (self.version, self.options["dialog"]["dont_notify_about_this_version"]): + if new_version[0] not in (self.version, self.options[0]["dialog"]["dont_notify_about_this_version"]): website = "https://github.com/comictagger/comictagger" checked = OptionalMessageDialog.msg( self, @@ -2150,7 +2155,7 @@ Have fun! "Don't tell me about this version again", ) if checked: - self.options["dialog"]["dont_notify_about_this_version"] = new_version[0] + self.options[0]["dialog"]["dont_notify_about_this_version"] = new_version[0] def on_incoming_socket_connection(self) -> None: # Accept connection from other instance. diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py index 03ff093..87c901f 100644 --- a/comictaggerlib/volumeselectionwindow.py +++ b/comictaggerlib/volumeselectionwindow.py @@ -19,13 +19,13 @@ import itertools import logging from collections import deque +import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic from PyQt5.QtCore import pyqtSignal from comicapi import utils from comicapi.comicarchive import ComicArchive from comicapi.genericmetadata import GenericMetadata -from comictaggerlib import settings from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.issueidentifier import IssueIdentifier from comictaggerlib.issueselectionwindow import IssueSelectionWindow @@ -111,7 +111,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog): issue_count: int, cover_index_list: list[int], comic_archive: ComicArchive | None, - options: settings.OptionValues, + options: settngs.ConfigValues, talker_api: ComicTalker, autoselect: bool = False, literal: bool = False, diff --git a/requirements.txt b/requirements.txt index 972345f..f36c9c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ pycountry pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* +settngs==0.2.0 text2digits typing_extensions wordninja diff --git a/testing/settings.py b/testing/settings.py deleted file mode 100644 index dc0fe31..0000000 --- a/testing/settings.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import annotations - -settings_cases = [ - ( - ( - ("--test",), - dict( - action=None, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=None, - help=None, - metavar=None, - dest=None, - cmdline=True, - file=True, - group="tst", - exclusive=False, - ), - ), # Equivalent to Setting("--test", group="tst") - { - "action": None, - "choices": None, - "cmdline": True, - "const": None, - "default": None, - "dest": "test", # dest is calculated by Setting and is not used by argparse - "exclusive": False, - "file": True, - "group": "tst", - "help": None, - "internal_name": "tst_test", # Should almost always be "{group}_{dest}" - "metavar": "TEST", # Set manually so argparse doesn't use TST_TEST - "nargs": None, - "required": None, - "type": None, - "argparse_args": ("--test",), # *args actually sent to argparse - "argparse_kwargs": { - "action": None, - "choices": None, - "const": None, - "default": None, - "dest": "tst_test", - "help": None, - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - }, # Non-None **kwargs sent to argparse - }, - ), - ( - ( - ( - "-t", - "--test", - ), - dict( - action=None, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=None, - help=None, - metavar=None, - dest=None, - cmdline=True, - file=True, - group="tst", - exclusive=False, - ), - ), # Equivalent to Setting("-t", "--test", group="tst") - { - "action": None, - "choices": None, - "cmdline": True, - "const": None, - "default": None, - "dest": "test", - "exclusive": False, - "file": True, - "group": "tst", - "help": None, - "internal_name": "tst_test", - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - "argparse_args": ( - "-t", - "--test", - ), # Only difference with above is here - "argparse_kwargs": { - "action": None, - "choices": None, - "const": None, - "default": None, - "dest": "tst_test", - "help": None, - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - }, - }, - ), - ( - ( - ("test",), - dict( - action=None, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=None, - help=None, - metavar=None, - dest=None, - cmdline=True, - file=True, - group="tst", - exclusive=False, - ), - ), # Equivalent to Setting("test", group="tst") - { - "action": None, - "choices": None, - "cmdline": True, - "const": None, - "default": None, - "dest": "test", - "exclusive": False, - "file": True, - "group": "tst", - "help": None, - "internal_name": "tst_test", - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - "argparse_args": ("tst_test",), - "argparse_kwargs": { - "action": None, - "choices": None, - "const": None, - "default": None, - "dest": None, # Only difference with #1 is here, argparse sets dest based on the *args passed to it - "help": None, - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - }, - }, - ), - ( - ( - ("--test",), - dict( - action=None, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=None, - help=None, - metavar=None, - dest=None, - cmdline=True, - file=True, - group="", - exclusive=False, - ), - ), # Equivalent to Setting("test") - { - "action": None, - "choices": None, - "cmdline": True, - "const": None, - "default": None, - "dest": "test", - "exclusive": False, - "file": True, - "group": "", - "help": None, - "internal_name": "test", # No group, leading _ is stripped - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - "argparse_args": ("--test",), - "argparse_kwargs": { - "action": None, - "choices": None, - "const": None, - "default": None, - "dest": "test", - "help": None, - "metavar": "TEST", - "nargs": None, - "required": None, - "type": None, - }, - }, - ), -] diff --git a/tests/comiccacher_test.py b/tests/comiccacher_test.py index 547f699..a9f87c2 100644 --- a/tests/comiccacher_test.py +++ b/tests/comiccacher_test.py @@ -6,9 +6,9 @@ import comictalker.comiccacher from testing.comicdata import search_results -def test_create_cache(settings, mock_version): - comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0]) - assert (settings["runtime"]["config"].user_cache_dir).exists() +def test_create_cache(options, mock_version): + comictalker.comiccacher.ComicCacher(options["runtime"]["config"].user_cache_dir, mock_version[0]) + assert (options["runtime"]["config"].user_cache_dir).exists() def test_search_results(comic_cache): diff --git a/tests/conftest.py b/tests/conftest.py index bde5491..b5dc79b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,15 +9,15 @@ from typing import Any import pytest import requests +import settngs from PIL import Image import comicapi.comicarchive import comicapi.genericmetadata -import comictaggerlib.settings +import comictaggerlib.ctoptions import comictalker.comiccacher import comictalker.talkers.comicvine from comicapi import utils -from comictaggerlib import settings as ctsettings from testing import comicvine, filenames from testing.comicdata import all_seed_imprints, seed_imprints @@ -56,7 +56,7 @@ def no_requests(monkeypatch) -> None: @pytest.fixture def comicvine_api( - monkeypatch, cbz, comic_cache, mock_version, settings + monkeypatch, cbz, comic_cache, mock_version, options ) -> comictalker.talkers.comicvine.ComicVineTalker: # Any arguments may be passed and mock_get() will always return our # mocked object, which only has the .json() method or None for invalid urls. @@ -117,7 +117,7 @@ def comicvine_api( cv = comictalker.talkers.comicvine.ComicVineTalker( version=mock_version[0], - cache_folder=settings["runtime"]["config"].user_cache_dir, + cache_folder=options["runtime"]["config"].user_cache_dir, api_url="", api_key="", series_match_thresh=90, @@ -163,12 +163,12 @@ def seed_all_publishers(monkeypatch): @pytest.fixture -def settings(settings_manager, tmp_path): +def options(settings_manager, tmp_path): - ctsettings.register_commandline(settings_manager) - ctsettings.register_settings(settings_manager) + comictaggerlib.ctoptions.register_commandline(settings_manager) + comictaggerlib.ctoptions.register_settings(settings_manager) defaults = settings_manager.defaults() - defaults["runtime"]["config"] = ctsettings.ComicTaggerPaths(tmp_path / "config") + defaults["runtime"]["config"] = comictaggerlib.ctoptions.ComicTaggerPaths(tmp_path / "config") defaults["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True) defaults["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True) defaults["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True) @@ -179,10 +179,10 @@ def settings(settings_manager, tmp_path): @pytest.fixture def settings_manager(): - manager = ctsettings.Manager() + manager = settngs.Manager() yield manager @pytest.fixture -def comic_cache(settings, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]: - yield comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0]) +def comic_cache(options, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]: + yield comictalker.comiccacher.ComicCacher(options["runtime"]["config"].user_cache_dir, mock_version[0]) diff --git a/tests/issueidentifier_test.py b/tests/issueidentifier_test.py index a4e3498..bf970cb 100644 --- a/tests/issueidentifier_test.py +++ b/tests/issueidentifier_test.py @@ -9,8 +9,8 @@ import testing.comicdata import testing.comicvine -def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api): - ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz_double_cover, settings, comicvine_api) +def test_crop(cbz_double_cover, options, tmp_path, comicvine_api): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz_double_cover, options, comicvine_api) cropped = ii.crop_cover(cbz_double_cover.archiver.read_file("double_cover.jpg")) original_cover = cbz_double_cover.get_page(0) @@ -21,15 +21,15 @@ def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api): @pytest.mark.parametrize("additional_md, expected", testing.comicdata.metadata_keys) -def test_get_search_keys(cbz, settings, additional_md, expected, comicvine_api): - ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api) +def test_get_search_keys(cbz, options, additional_md, expected, comicvine_api): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, options, comicvine_api) ii.set_additional_metadata(additional_md) assert expected == ii.get_search_keys() -def test_get_issue_cover_match_score(cbz, settings, comicvine_api): - ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api) +def test_get_issue_cover_match_score(cbz, options, comicvine_api): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, options, comicvine_api) score = ii.get_issue_cover_match_score( int( comicapi.issuestring.IssueString( @@ -49,8 +49,8 @@ def test_get_issue_cover_match_score(cbz, settings, comicvine_api): assert expected == score -def test_search(cbz, settings, comicvine_api): - ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api) +def test_search(cbz, options, comicvine_api): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, options, comicvine_api) results = ii.search() cv_expected = { "series": f"{testing.comicvine.cv_volume_result['results']['name']} ({testing.comicvine.cv_volume_result['results']['start_year']})", diff --git a/tests/settings_test.py b/tests/settings_test.py deleted file mode 100644 index 7072555..0000000 --- a/tests/settings_test.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -import argparse -import json - -import pytest - -import comictaggerlib.settings.manager -from testing.settings import settings_cases - - -def test_settings_manager(): - manager = comictaggerlib.settings.manager.Manager() - defaults = manager.defaults() - assert manager is not None and defaults is not None - - -@pytest.mark.parametrize("arguments, expected", settings_cases) -def test_setting(arguments, expected): - assert vars(comictaggerlib.settings.manager.Setting(*arguments[0], **arguments[1])) == expected - - -def test_add_setting(settings_manager): - assert settings_manager.add_setting("--test") is None - - -def test_get_defaults(settings_manager): - settings_manager.add_setting("--test", default="hello") - defaults = settings_manager.defaults() - assert defaults[""]["test"] == "hello" - - -def test_get_namespace(settings_manager): - settings_manager.add_setting("--test", default="hello") - defaults = settings_manager.get_namespace(settings_manager.defaults()) - assert defaults.test == "hello" - - -def test_get_defaults_group(settings_manager): - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello")) - defaults = settings_manager.defaults() - assert defaults["tst"]["test"] == "hello" - - -def test_get_namespace_group(settings_manager): - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello")) - defaults = settings_manager.get_namespace(settings_manager.defaults()) - assert defaults.tst_test == "hello" - - -def test_cmdline_only(settings_manager): - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello", file=False)) - settings_manager.add_group("tst2", lambda parser: parser.add_setting("--test2", default="hello", cmdline=False)) - - file_normalized = settings_manager.normalize_options({}, file=True) - cmdline_normalized = settings_manager.normalize_options({}, cmdline=True) - - assert "test" in cmdline_normalized["tst"] - assert "test2" not in cmdline_normalized["tst2"] - - assert "test" not in file_normalized["tst"] - assert "test2" in file_normalized["tst2"] - - -def test_normalize(settings_manager): - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello")) - - defaults = settings_manager.defaults() - defaults["test"] = "fail" # Not defined in settings_manager - - defaults_namespace = settings_manager.get_namespace(defaults) - defaults_namespace.test = "fail" - - normalized = settings_manager.normalize_options(defaults, file=True) - normalized_namespace = settings_manager.get_namespace(settings_manager.normalize_options(defaults, file=True)) - - assert "test" not in normalized - assert "tst" in normalized - assert "test" in normalized["tst"] - assert normalized["tst"]["test"] == "hello" - - assert not hasattr(normalized_namespace, "test") - assert hasattr(normalized_namespace, "tst_test") - assert normalized_namespace.tst_test == "hello" - - -@pytest.mark.parametrize( - "raw, raw2, expected", - [ - ({"tst": {"test": "fail"}}, argparse.Namespace(tst_test="success"), "success"), - # hello is default so is not used in raw_options_2 - ({"tst": {"test": "success"}}, argparse.Namespace(tst_test="hello"), "success"), - (argparse.Namespace(tst_test="fail"), {"tst": {"test": "success"}}, "success"), - (argparse.Namespace(tst_test="success"), {"tst": {"test": "hello"}}, "success"), - ], -) -def test_normalize_merge(raw, raw2, expected, settings_manager): - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello")) - - normalized = settings_manager.normalize_options(raw, file=True, raw_options_2=raw2) - - assert normalized["tst"]["test"] == expected - - -def test_cli_set(settings_manager, tmp_path): - settings_file = tmp_path / "settings.json" - settings_file.write_text(json.dumps({})) - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello", file=False)) - - normalized = settings_manager.parse_options(settings_file, ["--test", "success"]) - - assert "test" in normalized["tst"] - assert normalized["tst"]["test"] == "success" - - -def test_file_set(settings_manager, tmp_path): - settings_file = tmp_path / "settings.json" - settings_file.write_text( - json.dumps( - { - "tst": {"test": "success"}, - } - ) - ) - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello", cmdline=False)) - - normalized = settings_manager.parse_options(settings_file, []) - - assert "test" in normalized["tst"] - assert normalized["tst"]["test"] == "success" - - -def test_cli_override_file(settings_manager, tmp_path): - settings_file = tmp_path / "settings.json" - settings_file.write_text(json.dumps({"tst": {"test": "fail"}})) - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="hello")) - - normalized = settings_manager.parse_options(settings_file, ["--test", "success"]) - - assert "test" in normalized["tst"] - assert normalized["tst"]["test"] == "success" - - -def test_cli_explicit_default(settings_manager, tmp_path): - settings_file = tmp_path / "settings.json" - settings_file.write_text(json.dumps({"tst": {"test": "fail"}})) - settings_manager.add_group("tst", lambda parser: parser.add_setting("--test", default="success")) - - normalized = settings_manager.parse_options(settings_file, ["--test", "success"]) - - assert "test" in normalized["tst"] - assert normalized["tst"]["test"] == "success"