From 3d443e0908635e3c617673259b907f61dc2058bb Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 10 May 2024 23:51:13 +0100 Subject: [PATCH] lordwelch rewrite --- comicapi/genericmetadata.py | 213 ++++-------------- comicapi/merge.py | 128 +++++++++++ comicapi/metadata/comet.py | 28 +-- comicapi/metadata/comicbookinfo.py | 10 +- comicapi/metadata/comicrack.py | 28 +-- comictaggerlib/cbltransformer.py | 8 +- comictaggerlib/cli.py | 6 +- comictaggerlib/ctsettings/commandline.py | 8 +- comictaggerlib/ctsettings/file.py | 7 +- .../ctsettings/settngs_namespace.py | 14 +- comictaggerlib/settingswindow.py | 12 +- comictaggerlib/taggerwindow.py | 6 +- comictaggerlib/ui/settingswindow.ui | 6 +- testing/comicdata.py | 110 +++++++-- tests/genericmetadata_test.py | 66 +----- tests/integration_test.py | 10 +- 16 files changed, 341 insertions(+), 319 deletions(-) create mode 100644 comicapi/merge.py diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index cf8c4cc..9aec972 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -24,14 +24,12 @@ from __future__ import annotations import copy import dataclasses import logging -import sys from collections.abc import Sequence -from enum import Enum, auto -from typing import TYPE_CHECKING, Any, TypedDict, Union +from typing import TYPE_CHECKING, Any, TypedDict, Union, overload from typing_extensions import NamedTuple, Required -from comicapi import utils +from comicapi import merge, utils from comicapi._url import Url, parse_url from comicapi.utils import norm_fold @@ -41,50 +39,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -class __remove(Enum): - REMOVE = auto() - - -REMOVE = __remove.REMOVE - - -if sys.version_info < (3, 11): - - class StrEnum(str, Enum): - """ - Enum where members are also (and must be) strings - """ - - def __new__(cls, *values: Any) -> Any: - "values must already be of type `str`" - if len(values) > 3: - raise TypeError(f"too many arguments for str(): {values!r}") - if len(values) == 1: - # it must be a string - if not isinstance(values[0], str): - raise TypeError(f"{values[0]!r} is not a string") - if len(values) >= 2: - # check that encoding argument is a string - if not isinstance(values[1], str): - raise TypeError(f"encoding must be a string, not {values[1]!r}") - if len(values) == 3: - # check that errors argument is a string - if not isinstance(values[2], str): - raise TypeError("errors must be a string, not %r" % (values[2])) - value = str(*values) - member = str.__new__(cls, value) - member._value_ = value - return member - - @staticmethod - def _generate_next_value_(name: str, start: int, count: int, last_values: Any) -> str: - """ - Return the lower-cased version of the member name. - """ - return name.lower() - -else: - from enum import StrEnum +REMOVE = object() class PageType: @@ -117,10 +72,7 @@ class ImageMetadata(TypedDict, total=False): width: str -class Credit(TypedDict): - person: str - role: str - primary: bool +Credit = merge.Credit @dataclasses.dataclass @@ -145,12 +97,6 @@ class TagOrigin(NamedTuple): name: str -class OverlayMode(StrEnum): - overlay = auto() - add_missing = auto() - combine = auto() - - @dataclasses.dataclass class GenericMetadata: writer_synonyms = ("writer", "plotter", "scripter", "script") @@ -261,93 +207,19 @@ class GenericMetadata: new_md.__post_init__() return new_md - def credit_dedupe(self, cur: list[Credit], new: list[Credit]) -> list[Credit]: - if len(new) == 0: - return cur - if len(cur) == 0: - return new - - # Create dict for deduplication - new_dict: dict[str, Credit] = {norm_fold(f"{n['person']}_{n['role']}"): n for n in new} - cur_dict: dict[str, Credit] = {norm_fold(f"{c['person']}_{c['role']}"): c for c in cur} - - # Any duplicates use the 'new' value - cur_dict.update(new_dict) - return list(cur_dict.values()) - - def assign_dedupe(self, new: list[str] | set[str], cur: list[str] | set[str]) -> list[str] | set[str]: - """Dedupes normalised (NFKD), casefolded values using 'new' values on collisions""" - if len(new) == 0: - return cur - if len(cur) == 0: - return new - - # Create dict values for deduplication - new_dict: dict[str, str] = {norm_fold(n): n for n in new} - cur_dict: dict[str, str] = {norm_fold(c): c for c in cur} - - if isinstance(cur, list): - cur_dict.update(new_dict) - return list(cur_dict.values()) - - if isinstance(cur, set): - cur_dict.update(new_dict) - return set(cur_dict.values()) - - # All else fails - return cur - - def assign_overlay(self, cur: Any, new: Any) -> Any: - """Overlay - When the 'new' object has non-None values, overwrite 'cur'(rent) with 'new'.""" - if new is None: - return cur - if isinstance(new, (list, set)) and len(new) == 0: - return cur - else: - return new - - def assign_add_missing(self, cur: Any, new: Any) -> Any: - """Add Missing - Any 'cur(rent)' values that are None or an empty list/set, add 'new' non-None values""" - if new is None: - return cur - if cur is None: - return new - elif isinstance(cur, (list, set)) and len(cur) == 0: - return new - else: - return cur - - def assign_combine(self, cur: Any, new: Any) -> Any: - """Combine - Combine lists and sets (checking for dupes). All non-None values from new replace cur(rent)""" - if new is None: - return cur - if isinstance(new, (list, set)) and isinstance(cur, (list, set)): - # Check for weblinks (Url is a tuple) - if len(new) > 0 and isinstance(new, list) and isinstance(new[0], Url): - return list(set(new).union(cur)) - return self.assign_dedupe(new, cur) - else: - return new - - def overlay(self, new_md: GenericMetadata, mode: OverlayMode = OverlayMode.overlay) -> None: + def overlay(self, new_md: GenericMetadata, mode: merge.Mode = merge.Mode.OVERLAY) -> None: """Overlay a new metadata object on this one""" - assign_funcs = { - OverlayMode.overlay: self.assign_overlay, - OverlayMode.add_missing: self.assign_add_missing, - OverlayMode.combine: self.assign_combine, - } + merge_function = merge.function[mode] def assign(cur: Any, new: Any) -> Any: if new is REMOVE: if isinstance(cur, (list, set)): cur.clear() return cur - else: - return None + return None - assign_func = assign_funcs.get(mode, self.assign_overlay) - return assign_func(cur, new) + return merge_function(cur, new) if not new_md.is_empty: self.is_empty = False @@ -390,13 +262,11 @@ class GenericMetadata: self.scan_info = assign(self.scan_info, new_md.scan_info) self.tags = assign(self.tags, new_md.tags) - self.pages = assign(self.pages, new_md.pages) - self.page_count = assign(self.page_count, new_md.page_count) self.characters = assign(self.characters, new_md.characters) self.teams = assign(self.teams, new_md.teams) self.locations = assign(self.locations, new_md.locations) - self.credits = self.assign_credits(self.credits, new_md.credits) + [self.add_credit(c) for c in assign(self.credits, new_md.credits)] self.price = assign(self.price, new_md.price) self.is_version_of = assign(self.is_version_of, new_md.is_version_of) @@ -406,27 +276,8 @@ class GenericMetadata: self._cover_image = assign(self._cover_image, new_md._cover_image) self._alternate_images = assign(self._alternate_images, new_md._alternate_images) - def assign_credits_overlay(self, cur_credits: list[Credit], new_credits: list[Credit]) -> list[Credit]: - return self.credit_dedupe(cur_credits, new_credits) - - def assign_credits_add_missing(self, cur_credits: list[Credit], new_credits: list[Credit]) -> list[Credit]: - # Send new_credits as cur_credits and vis-versa to keep cur_credit on duplication - return self.credit_dedupe(new_credits, cur_credits) - - def assign_credits( - self, cur_credits: list[Credit], new_credits: list[Credit], mode: OverlayMode = OverlayMode.overlay - ) -> list[Credit]: - if new_credits is REMOVE: - return [] - - assign_credit_funcs = { - OverlayMode.overlay: self.assign_credits_overlay, - OverlayMode.add_missing: self.assign_credits_add_missing, - OverlayMode.combine: self.assign_credits_overlay, - } - - assign_credit_func = assign_credit_funcs.get(mode, self.assign_credits_overlay) - return assign_credit_func(cur_credits, new_credits) + self.pages = assign(self.pages, new_md.pages) + self.page_count = assign(self.page_count, new_md.page_count) def apply_default_page_list(self, page_list: Sequence[str]) -> None: # generate a default page list, with the first page marked as the cover @@ -471,21 +322,35 @@ class GenericMetadata: return coverlist - def add_credit(self, person: str, role: str, primary: bool = False) -> None: - if person == "": + @overload + def add_credit(self, person: Credit) -> None: ... + + @overload + def add_credit(self, person: str, role: str, primary: bool = False) -> None: ... + + def add_credit(self, person: str | Credit, role: str | None = None, primary: bool = False) -> None: + + credit: Credit + if isinstance(person, Credit): + credit = person + else: + assert role is not None + credit = Credit(person=person, role=role, primary=primary) + + if credit.role is None: + raise TypeError("GenericMetadata.add_credit takes either a Credit object or a person name and role") + if credit.person == "": return - credit = Credit(person=person, role=role, primary=primary) - - person = norm_fold(person) - role = norm_fold(role) + person = norm_fold(credit.person) + role = norm_fold(credit.role) # look to see if it's not already there... found = False for c in self.credits: - if norm_fold(c["person"]) == person and norm_fold(c["role"]) == role: + if norm_fold(c.person) == person and norm_fold(c.role) == role: # no need to add it. just adjust the "primary" flag as needed - c["primary"] = primary + c.primary = c.primary or primary found = True break @@ -495,12 +360,10 @@ class GenericMetadata: def get_primary_credit(self, role: str) -> str: primary = "" for credit in self.credits: - if "role" not in credit or "person" not in credit: - continue - if (primary == "" and credit["role"].casefold() == role.casefold()) or ( - credit["role"].casefold() == role.casefold() and "primary" in credit and credit["primary"] + if (primary == "" and credit.role.casefold() == role.casefold()) or ( + credit.role.casefold() == role.casefold() and credit.primary ): - primary = credit["person"] + primary = credit.person return primary def __str__(self) -> str: @@ -559,9 +422,9 @@ class GenericMetadata: for c in self.credits: primary = "" - if "primary" in c and c["primary"]: + if c.primary: primary = " [P]" - add_string("credit", c["role"] + ": " + c["person"] + primary) + add_string("credit", c.role + ": " + c.person + primary) # find the longest field name flen = 0 diff --git a/comicapi/merge.py b/comicapi/merge.py new file mode 100644 index 0000000..16e773d --- /dev/null +++ b/comicapi/merge.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import dataclasses +import sys +from collections import defaultdict +from collections.abc import Collection +from enum import Enum, auto +from typing import Any + +from comicapi.utils import norm_fold + +if sys.version_info < (3, 11): + + class StrEnum(str, Enum): + """ + Enum where members are also (and must be) strings + """ + + def __new__(cls, *values: Any) -> Any: + "values must already be of type `str`" + if len(values) > 3: + raise TypeError(f"too many arguments for str(): {values!r}") + if len(values) == 1: + # it must be a string + if not isinstance(values[0], str): + raise TypeError(f"{values[0]!r} is not a string") + if len(values) >= 2: + # check that encoding argument is a string + if not isinstance(values[1], str): + raise TypeError(f"encoding must be a string, not {values[1]!r}") + if len(values) == 3: + # check that errors argument is a string + if not isinstance(values[2], str): + raise TypeError("errors must be a string, not %r" % (values[2])) + value = str(*values) + member = str.__new__(cls, value) + member._value_ = value + return member + + @staticmethod + def _generate_next_value_(name: str, start: int, count: int, last_values: Any) -> str: + """ + Return the lower-cased version of the member name. + """ + return name.lower() + +else: + from enum import StrEnum + + +@dataclasses.dataclass +class Credit: + person: str = "" + role: str = "" + primary: bool = False + + +class Mode(StrEnum): + OVERLAY = auto() + ADD_MISSING = auto() + COMBINE = auto() + + +def merge_lists(old: Collection[Any], new: Collection[Any]) -> list[Any] | set[Any]: + """Dedupes normalised (NFKD), casefolded values using 'new' values on collisions""" + if len(new) == 0: + return old if isinstance(old, set) else list(old) + if len(old) == 0: + return new if isinstance(new, set) else list(new) + + # Create dict to preserve case + new_dict = {norm_fold(str(n)): n for n in new} + old_dict = {norm_fold(str(c)): c for c in old} + + old_dict.update(new_dict) + + if isinstance(old, set): + return set(old_dict.values()) + + return list(old_dict.values()) + + +def combine(old: Any, new: Any) -> Any: + """combine - Same as `overlay` except lists are merged""" + if new is None: + return old + + if ( + not (isinstance(new, str) or isinstance(old, str)) + and isinstance(new, Collection) + and isinstance(old, Collection) + ): + return merge_lists(old, new) + if isinstance(new, str) and len(new) == 0: + return old + return new + + +def overlay(old: Any, new: Any) -> Any: + """overlay - When the `new` object is not empty, replace `old` with `new`.""" + if new is None: + return old + if ( + not (isinstance(new, str) or isinstance(old, str)) + and isinstance(new, Collection) + and isinstance(old, Collection) + ): + if isinstance(new, list) and len(new) > 0 and isinstance(new[0], Credit): + return merge_lists(old, new) + if len(new) == 0: + return old + + return new + + +def add_missing(old: Any, new: Any) -> Any: + """add_missing - When the `old` object is empty, replace `old` with `new`""" + return overlay(new, old) + + +function = defaultdict( + lambda: overlay, + { + Mode.OVERLAY: overlay, + Mode.ADD_MISSING: add_missing, + Mode.COMBINE: combine, + }, +) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index a521784..aa58a12 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -199,26 +199,26 @@ class CoMet(Metadata): # loop thru credits, and build a list for each role that CoMet supports for credit in metadata.credits: - if credit["role"].casefold() in set(GenericMetadata.writer_synonyms): - ET.SubElement(root, "writer").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.writer_synonyms): + ET.SubElement(root, "writer").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.penciller_synonyms): - ET.SubElement(root, "penciller").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.penciller_synonyms): + ET.SubElement(root, "penciller").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.inker_synonyms): - ET.SubElement(root, "inker").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.inker_synonyms): + ET.SubElement(root, "inker").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.colorist_synonyms): - ET.SubElement(root, "colorist").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.colorist_synonyms): + ET.SubElement(root, "colorist").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.letterer_synonyms): - ET.SubElement(root, "letterer").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.letterer_synonyms): + ET.SubElement(root, "letterer").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.cover_synonyms): - ET.SubElement(root, "coverDesigner").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.cover_synonyms): + ET.SubElement(root, "coverDesigner").text = str(credit.person) - if credit["role"].casefold() in set(GenericMetadata.editor_synonyms): - ET.SubElement(root, "editor").text = str(credit["person"]) + if credit.role.casefold() in set(GenericMetadata.editor_synonyms): + ET.SubElement(root, "editor").text = str(credit.person) ET.indent(root) diff --git a/comicapi/metadata/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py index 3c49d82..9e62273 100644 --- a/comicapi/metadata/comicbookinfo.py +++ b/comicapi/metadata/comicbookinfo.py @@ -47,6 +47,12 @@ _CBILiteralType = Literal[ ] +class credit(TypedDict): + person: str + role: str + primary: bool + + class _ComicBookInfoJson(TypedDict, total=False): series: str title: str @@ -61,7 +67,7 @@ class _ComicBookInfoJson(TypedDict, total=False): genre: str language: str country: str - credits: list[Credit] + credits: list[credit] tags: list[str] comments: str @@ -217,7 +223,7 @@ class ComicBookInfo(Metadata): assign("language", utils.xlate(utils.get_language_from_iso(metadata.language))) assign("country", utils.xlate(metadata.country)) assign("rating", utils.xlate_int(metadata.critical_rating)) - assign("credits", metadata.credits) + assign("credits", [credit(person=c.person, role=c.role, primary=c.primary) for c in metadata.credits]) assign("tags", list(metadata.tags)) return cbi_container diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 1b02826..b6a7b78 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -187,26 +187,26 @@ class ComicRack(Metadata): # first, loop thru credits, and build a list for each role that CIX # supports for credit in metadata.credits: - if credit["role"].casefold() in set(GenericMetadata.writer_synonyms): - credit_writer_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.writer_synonyms): + credit_writer_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.penciller_synonyms): - credit_penciller_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.penciller_synonyms): + credit_penciller_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.inker_synonyms): - credit_inker_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.inker_synonyms): + credit_inker_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.colorist_synonyms): - credit_colorist_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.colorist_synonyms): + credit_colorist_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.letterer_synonyms): - credit_letterer_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.letterer_synonyms): + credit_letterer_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.cover_synonyms): - credit_cover_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.cover_synonyms): + credit_cover_list.append(credit.person.replace(",", "")) - if credit["role"].casefold() in set(GenericMetadata.editor_synonyms): - credit_editor_list.append(credit["person"].replace(",", "")) + if credit.role.casefold() in set(GenericMetadata.editor_synonyms): + credit_editor_list.append(credit.person.replace(",", "")) assign("Series", md.series) assign("Number", md.issue) diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index d195273..3b38232 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -36,14 +36,14 @@ class CBLTransformer: lone_credit: Credit | None = None count = 0 for c in self.metadata.credits: - if c["role"].casefold() in role_list: + if c.role.casefold() in role_list: count += 1 lone_credit = c if count > 1: lone_credit = None break if lone_credit is not None: - lone_credit["primary"] = True + lone_credit.primary = True return lone_credit, count # need to loop three times, once for 'writer', 'artist', and then @@ -53,8 +53,8 @@ class CBLTransformer: if c is None and count == 0: c, count = set_lone_primary(["penciler", "penciller"]) if c is not None: - c["primary"] = False - self.metadata.add_credit(c["person"], "Artist", True) + c.primary = False + self.metadata.add_credit(c.person, "Artist", True) if self.config.Metadata_Options__cbl_copy_characters_to_tags: self.metadata.tags.update(x for x in self.metadata.characters) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 6e7aa2c..b449780 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -26,10 +26,10 @@ import sys from collections.abc import Collection from typing import Any, TextIO -from comicapi import utils +from comicapi import merge, utils from comicapi.comicarchive import ComicArchive from comicapi.comicarchive import metadata_styles as md_styles -from comicapi.genericmetadata import GenericMetadata, OverlayMode +from comicapi.genericmetadata import GenericMetadata from comictaggerlib.cbltransformer import CBLTransformer from comictaggerlib.ctsettings import ct_ns from comictaggerlib.filerenamer import FileRenamer, get_rename_dir @@ -259,7 +259,7 @@ class CLI: logger.error("Failed to load metadata for %s: %s", ca.path, e) # finally, use explicit stuff (always 'overlay' mode) - md.overlay(self.config.Runtime_Options__metadata, mode=OverlayMode.overlay) + md.overlay(self.config.Runtime_Options__metadata, mode=merge.Mode.OVERLAY) return md diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index 8d8a04c..02a4747 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -25,9 +25,9 @@ import subprocess import settngs -from comicapi import utils +from comicapi import merge, utils from comicapi.comicarchive import metadata_styles -from comicapi.genericmetadata import GenericMetadata, OverlayMode +from comicapi.genericmetadata import GenericMetadata from comictaggerlib import ctversion from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns from comictaggerlib.ctsettings.types import ( @@ -179,13 +179,13 @@ def register_runtime(parser: settngs.Manager) -> None: ) parser.add_setting( "--read-style-overlay", - type=OverlayMode, + type=merge.Mode, help="How to overlay new metadata on the current for enabled read styles (CR, CBL, etc.)", file=False, ) parser.add_setting( "--source-overlay", - type=OverlayMode, + type=merge.Mode, help="How to overlay new metadata from a data source (CV, Metron, GCD, etc.) on to the current", file=False, ) diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 83e534a..b10adf5 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -5,8 +5,7 @@ import uuid import settngs -from comicapi import utils -from comicapi.genericmetadata import OverlayMode +from comicapi import merge, utils from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements @@ -27,8 +26,8 @@ def internal(parser: settngs.Manager) -> None: parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False) parser.add_setting("save_data_style", default=["cbi"], cmdline=False) parser.add_setting("load_data_style", default=["cbi"], cmdline=False) - parser.add_setting("load_data_overlay", default=OverlayMode.overlay, cmdline=False, type=OverlayMode) - parser.add_setting("source_data_overlay", default=OverlayMode.overlay, cmdline=False, type=OverlayMode) + parser.add_setting("load_data_overlay", default=merge.Mode.OVERLAY, cmdline=False, type=merge.Mode) + parser.add_setting("source_data_overlay", default=merge.Mode.OVERLAY, cmdline=False, type=merge.Mode) parser.add_setting("last_opened_folder", default="", cmdline=False) parser.add_setting("window_width", default=0, cmdline=False) parser.add_setting("window_height", default=0, cmdline=False) diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index 6f85208..b03dc0b 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -5,11 +5,11 @@ import typing import settngs import comicapi.genericmetadata +import comicapi.merge import comicapi.utils import comictaggerlib.ctsettings.types import comictaggerlib.defaults import comictaggerlib.resulttypes -from comicapi.genericmetadata import OverlayMode class SettngsNS(settngs.TypedNS): @@ -37,8 +37,8 @@ class SettngsNS(settngs.TypedNS): Runtime_Options__json: bool Runtime_Options__type_modify: list[str] Runtime_Options__type_read: list[str] - Runtime_Options__read_style_overlay: OverlayMode - Runtime_Options__source_overlay: OverlayMode + Runtime_Options__read_style_overlay: comicapi.merge.Mode + Runtime_Options__source_overlay: comicapi.merge.Mode Runtime_Options__overwrite: bool Runtime_Options__no_gui: bool Runtime_Options__files: list[str] @@ -46,8 +46,8 @@ class SettngsNS(settngs.TypedNS): internal__install_id: str internal__save_data_style: list[str] internal__load_data_style: list[str] - internal__load_data_overlay: OverlayMode - internal__source_data_overlay: OverlayMode + internal__load_data_overlay: comicapi.merge.Mode + internal__source_data_overlay: comicapi.merge.Mode internal__last_opened_folder: str internal__window_width: int internal__window_height: int @@ -147,6 +147,8 @@ class Runtime_Options(typing.TypedDict): quiet: bool json: bool type: list[str] + read_style_overlay: comicapi.merge.Mode + source_overlay: comicapi.merge.Mode overwrite: bool no_gui: bool files: list[str] @@ -156,6 +158,8 @@ class internal(typing.TypedDict): install_id: str save_data_style: list[str] load_data_style: list[str] + load_data_overlay: comicapi.merge.Mode + source_data_overlay: comicapi.merge.Mode last_opened_folder: str window_width: int window_height: int diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 0b60b15..0169ad3 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -28,10 +28,9 @@ import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic import comictaggerlib.ui.talkeruigenerator -from comicapi import utils +from comicapi import merge, utils from comicapi.archivers.archiver import Archiver -# TODO OverlayMode changed to merge -from comicapi.genericmetadata import OverlayMode, md_test +from comicapi.genericmetadata import md_test from comictaggerlib import ctsettings from comictaggerlib.ctsettings import ct_ns from comictaggerlib.ctsettings.plugin import group_for_plugin @@ -195,8 +194,7 @@ class SettingsWindow(QtWidgets.QDialog): ) self.cbFilenameParser.clear() self.cbFilenameParser.addItems(utils.Parser) - # TODO check - for mode in OverlayMode: + for mode in merge.Mode: self.cbxOverlayReadStyle.addItem(mode.name.capitalize().replace("_", " "), mode.value) self.cbxOverlaySource.addItem(mode.name.capitalize().replace("_", " "), mode.value) self.connect_signals() @@ -570,8 +568,8 @@ class SettingsWindow(QtWidgets.QDialog): self.cbxApplyCBLTransformOnBatchOperation.isChecked() ) - self.config[0].internal__load_data_overlay = OverlayMode[self.cbxOverlayReadStyle.currentData()] - self.config[0].internal__source_data_overlay = OverlayMode[self.cbxOverlaySource.currentData()] + self.config[0].internal__load_data_overlay = merge.Mode[self.cbxOverlayReadStyle.currentData()] + self.config[0].internal__source_data_overlay = merge.Mode[self.cbxOverlaySource.currentData()] self.config[0].Metadata_Options__disable_cr = self.cbxDisableCR.isChecked() # Update metadata style names if required if self.config[0].Metadata_Options__use_short_metadata_names != self.cbxShortMetadataNames.isChecked(): diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index a69044a..108e539 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -939,12 +939,10 @@ class TaggerWindow(QtWidgets.QMainWindow): for row, credit in enumerate(md.credits): # if the role-person pair already exists, just skip adding it to the list - if self.is_dupe_credit(credit["role"].title(), credit["person"]): + if self.is_dupe_credit(credit.role.title(), credit.person): continue - self.add_new_credit_entry( - row, credit["role"].title(), credit["person"], (credit["primary"] if "primary" in credit else False) - ) + self.add_new_credit_entry(row, credit.role.title(), credit.person, credit.primary) self.twCredits.setSortingEnabled(True) self.update_metadata_credit_colors() diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index ca5d2a5..a3af0f7 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -755,7 +755,7 @@ false - Overlays all the none empty values of the read style/data source (e.g. Comic Vine) metadata on top of the current metadata (i.e. file name parsing, other read style(s)). + Overlays all the non-empty values of the new metadata (e.g. Comic Vine) on top of the current metadata. Example: (Series=batman, Issue=1, Tags=[batman,joker,robin]) @@ -792,7 +792,7 @@ Example: - Adds any metadata that is present in the read style/data source (e.g. Comic Vine) but is missing in the current metadata (i.e. file name parsing, other read style(s)). + Adds any metadata that is present in the new metadata (e.g. Comic Vine) but is missing in the current metadata. Example: (Series=batman, Issue=1, Tags=[batman,joker,robin]) @@ -829,7 +829,7 @@ Example: - Combine lists (tags, characters, genres, credits, etc.) replacing duplicates with the "new" data and all other non-empty values are replaced by the "new" values. + Same as Overlay except lists (tags, characters, genres, credits, etc.) are combined, replacing duplicates with the "new" data and all other non-empty values are replaced by the "new" values. Example: (Series=batman, Issue=1, Tags=[batman,joker,robin]) diff --git a/testing/comicdata.py b/testing/comicdata.py index fb50eaa..40602dd 100644 --- a/testing/comicdata.py +++ b/testing/comicdata.py @@ -55,23 +55,86 @@ select_details = [ # Used to test GenericMetadata.overlay metadata = [ ( + comicapi.genericmetadata.md_test.copy(), comicapi.genericmetadata.GenericMetadata(series="test", issue="2", title="never"), comicapi.genericmetadata.md_test.replace(series="test", issue="2", title="never"), ), ( + comicapi.genericmetadata.md_test.copy(), comicapi.genericmetadata.GenericMetadata(), comicapi.genericmetadata.md_test.copy(), ), + ( + comicapi.genericmetadata.GenericMetadata( + credits=[ + comicapi.genericmetadata.Credit(person="test", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test", role="artist", primary=True), + ], + ), + comicapi.genericmetadata.GenericMetadata( + credits=[ + comicapi.genericmetadata.Credit(person="", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test2", role="inker", primary=False), + ] + ), + comicapi.genericmetadata.GenericMetadata( + credits=[ + comicapi.genericmetadata.Credit(person="test", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test", role="artist", primary=True), + comicapi.genericmetadata.Credit(person="test2", role="inker", primary=False), + ] + ), + ), ] metadata_add = [ ( - comicapi.genericmetadata.GenericMetadata(series="test", issue="1", title="test", genres={"test", "test2"}), comicapi.genericmetadata.GenericMetadata( - series="test2", issue="2", title="test2", genres={"test3", "test4"}, issue_count=5 + series="test", + title="test", + issue="1", + volume_count=1, + page_count=3, + day=3, + genres={"test", "test2"}, + story_arcs=["arc"], + characters=set(), + credits=[ + comicapi.genericmetadata.Credit(person="test", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test", role="artist", primary=True), + ], ), comicapi.genericmetadata.GenericMetadata( - series="test", issue="1", title="test", genres={"test", "test2"}, issue_count=5 + series="test2", + title="", + issue_count=2, + page_count=2, + day=0, + genres={"fake"}, + characters={"bob", "fred"}, + scan_info="nothing", + credits=[ + comicapi.genericmetadata.Credit(person="Bob", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test", role="artist", primary=True), + ], + ), + comicapi.genericmetadata.GenericMetadata( + series="test", + title="test", + issue="1", + issue_count=2, + volume_count=1, + day=3, + page_count=3, + genres={"test", "test2"}, + story_arcs=["arc"], + characters={"bob", "fred"}, + scan_info="nothing", + credits=[ + comicapi.genericmetadata.Credit(person="test", role="writer", primary=False), + comicapi.genericmetadata.Credit(person="test", role="artist", primary=True), + comicapi.genericmetadata.Credit(person="Bob", role="writer", primary=False), + ], ), ), ] @@ -80,33 +143,42 @@ metadata_combine = [ ( comicapi.genericmetadata.GenericMetadata( series="test", - issue="1", title="test", + issue="1", + volume_count=1, + page_count=3, + day=3, genres={"test", "test2"}, - story_arcs=["arc1"], - characters={"Bob", "fred"}, + story_arcs=["arc"], + characters=set(), web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")], ), comicapi.genericmetadata.GenericMetadata( series="test2", - title="test2", - genres={"test2", "test3", "test4"}, - story_arcs=["arc1", "arc2"], + title="", + issue_count=2, + page_count=2, + day=0, + genres={"fake"}, characters={"bob", "fred"}, + scan_info="nothing", + web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")], ), comicapi.genericmetadata.GenericMetadata( series="test2", + title="test", issue="1", - title="test2", - genres={"test", "test2", "test3", "test4"}, - story_arcs=["arc1", "arc2"], + issue_count=2, + volume_count=1, + day=0, + page_count=2, + genres={"fake", "test", "test2"}, + story_arcs=["arc"], characters={"bob", "fred"}, + scan_info="nothing", web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")], ), ), -] - -metadata_dedupe_set = [ ( comicapi.genericmetadata.GenericMetadata(characters={"Macintosh", "Søren Kierkegaard", "Barry"}), comicapi.genericmetadata.GenericMetadata(characters={"MacIntosh", "Soren Kierkegaard"}), @@ -114,9 +186,6 @@ metadata_dedupe_set = [ characters={"MacIntosh", "Soren Kierkegaard", "Søren Kierkegaard", "Barry"} ), ), -] - -metadata_dedupe_list = [ ( comicapi.genericmetadata.GenericMetadata(story_arcs=["arc 1", "arc2", "arc 3"]), comicapi.genericmetadata.GenericMetadata(story_arcs=["Arc 1", "Arc2"]), @@ -146,7 +215,10 @@ credits = [ (comicapi.genericmetadata.md_test, "writeR", "Dara Naraghi"), ( comicapi.genericmetadata.md_test.replace( - credits=[{"person": "Dara Naraghi", "role": "writer"}, {"person": "Dara Naraghi", "role": "writer"}] + credits=[ + comicapi.genericmetadata.Credit(person="Dara Naraghi", role="writer"), + comicapi.genericmetadata.Credit(person="Dara Naraghi", role="writer"), + ] ), "writeR", "Dara Naraghi", diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py index 1d64918..7b090ee 100644 --- a/tests/genericmetadata_test.py +++ b/tests/genericmetadata_test.py @@ -5,8 +5,8 @@ import textwrap import pytest import comicapi.genericmetadata -import testing.comicdata as cd -from comicapi.genericmetadata import OverlayMode +import comicapi.merge +import testing.comicdata def test_apply_default_page_list(tmp_path): @@ -18,71 +18,25 @@ def test_apply_default_page_list(tmp_path): assert isinstance(md.pages[0]["image_index"], int) -@pytest.mark.parametrize("replaced, expected", cd.metadata) -def test_metadata_overlay(md: comicapi.genericmetadata.GenericMetadata, replaced, expected): - md.overlay(replaced) +@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata) +def test_metadata_overlay(md, new, expected): + md.overlay(new, comicapi.merge.Mode.OVERLAY) assert md == expected -@pytest.mark.parametrize("md, new, expected", cd.metadata_add) +@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata_add) def test_metadata_overlay_add_missing(md, new, expected): - md.overlay(new, OverlayMode.add_missing) + md.overlay(new, comicapi.merge.Mode.ADD_MISSING) assert md == expected -@pytest.mark.parametrize("md, new, expected", cd.metadata_combine) +@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata_combine) def test_metadata_overlay_combine(md, new, expected): - md.overlay(new, OverlayMode.combine) + md.overlay(new, comicapi.merge.Mode.COMBINE) assert md == expected -@pytest.mark.parametrize("md, new, expected", cd.metadata_dedupe_set) -def test_assign_dedupe_set(md, new, expected): - md.overlay(new, OverlayMode.combine) - assert md == expected - - -@pytest.mark.parametrize("md, new, expected", cd.metadata_dedupe_list) -def test_assign_dedupe_list(md, new, expected): - md.overlay(new, OverlayMode.combine) - assert md == expected - - -def test_assign_credits_overlay(): - md = comicapi.genericmetadata.GenericMetadata() - md.add_credit(person="test", role="writer", primary=False) - md.add_credit(person="test", role="artist", primary=True) - - md_new = comicapi.genericmetadata.GenericMetadata() - md_new.add_credit(person="", role="writer") - md_new.add_credit(person="test2", role="inker") - - expected = comicapi.genericmetadata.GenericMetadata() - expected.add_credit(person="test", role="writer", primary=False) - expected.add_credit(person="test", role="artist", primary=True) - expected.add_credit(person="test2", role="inker") - - assert md.assign_credits_overlay(md.credits, md_new.credits) == expected.credits - - -def test_assign_credits_add_missing(): - md = comicapi.genericmetadata.GenericMetadata() - md.add_credit(person="test", role="writer", primary=False) - md.add_credit(person="test", role="artist", primary=True) - - md_new = comicapi.genericmetadata.GenericMetadata() - md_new.add_credit(person="Bob", role="writer") - md_new.add_credit(person="test", role="artist", primary=True) - - expected = comicapi.genericmetadata.GenericMetadata() - expected.add_credit(person="Bob", role="writer") - expected.add_credit(person="test", role="artist", primary=True) - expected.add_credit(person="test", role="writer", primary=False) - - assert md.assign_credits_add_missing(md.credits, md_new.credits) == expected.credits - - def test_add_credit(): md = comicapi.genericmetadata.GenericMetadata() @@ -98,7 +52,7 @@ def test_add_credit_primary(): assert md.credits == [comicapi.genericmetadata.Credit(person="test", role="writer", primary=True)] -@pytest.mark.parametrize("md, role, expected", cd.credits) +@pytest.mark.parametrize("md, role, expected", testing.comicdata.credits) def test_get_primary_credit(md, role, expected): assert md.get_primary_credit(role) == expected diff --git a/tests/integration_test.py b/tests/integration_test.py index 57ff9c1..bf5bf9f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -54,11 +54,11 @@ def test_save( # unrelated to comicvine need to be re-worked md_saved.credits.insert( 1, - { - "person": "Esteve Polls", - "primary": False, - "role": "Writer", - }, + comicapi.genericmetadata.Credit( + person="Esteve Polls", + primary=False, + role="Writer", + ), ) # Validate that we got the correct metadata back