Split settings out into a separate package

This commit is contained in:
Timmy Welch 2022-12-14 23:13:53 -08:00
parent eca421e0f2
commit 103379e548
No known key found for this signature in database
26 changed files with 307 additions and 1062 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
},
},
),
]

View File

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

View File

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

View File

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

View File

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