From 6e7660c3d9d1a55bd5e79255b81846536aafe73e Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Jul 2022 09:00:56 -0700 Subject: [PATCH] Tests Add tests for IssueIdentifier Change tags to a set from a string Add copy and replace convenience functions on GenericMetadata Update deprecated resampling code for Pillow Change comicvine test data to be the same as the test comic Cleanup tests --- comicapi/comicbookinfo.py | 4 +- comicapi/genericmetadata.py | 21 ++++-- comictaggerlib/cbltransformer.py | 2 +- comictaggerlib/imagehasher.py | 2 +- comictaggerlib/taggerwindow.py | 6 +- testing/comicdata.py | 41 ++++++++++-- testing/comicvine.py | 109 ++++++++++++++++++++----------- tests/comicarchive_test.py | 53 +++++---------- tests/comicvinetalker_test.py | 75 ++++++++++++--------- tests/conftest.py | 71 +++++++++++++++++--- tests/genericmetadata_test.py | 4 +- tests/issueidentifier_test.py | 75 +++++++++++++++++++++ 12 files changed, 327 insertions(+), 136 deletions(-) create mode 100644 tests/issueidentifier_test.py diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py index 9201dfb..bfe1474 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -98,13 +98,11 @@ class ComicBookInfo: metadata.critical_rating = utils.xlate(cbi["rating"], True) metadata.credits = cbi["credits"] - metadata.tags = cbi["tags"] + metadata.tags = set(cbi["tags"]) if cbi["tags"] is not None else set() # make sure credits and tags are at least empty lists and not None if metadata.credits is None: metadata.credits = [] - if metadata.tags is None: - metadata.tags = [] # need the language string to be ISO if metadata.language is not None: diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 244ae9a..1c6e139 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -20,8 +20,9 @@ possible, however lossy it might be # limitations under the License. from __future__ import annotations +import copy +import dataclasses import logging -from dataclasses import dataclass, field from typing import Any, TypedDict from comicapi import utils @@ -65,7 +66,7 @@ class CreditMetadata(TypedDict): primary: bool -@dataclass +@dataclasses.dataclass class GenericMetadata: writer_synonyms = ["writer", "plotter", "scripter"] penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"] @@ -115,9 +116,9 @@ class GenericMetadata: teams: str | None = None locations: str | None = None - credits: list[CreditMetadata] = field(default_factory=list) - tags: list[str] = field(default_factory=list) - pages: list[ImageMetadata] = field(default_factory=list) + credits: list[CreditMetadata] = dataclasses.field(default_factory=list) + tags: set[str] = dataclasses.field(default_factory=set) + pages: list[ImageMetadata] = dataclasses.field(default_factory=list) # Some CoMet-only items price: str | None = None @@ -133,6 +134,14 @@ class GenericMetadata: self.is_empty = False break + def copy(self) -> GenericMetadata: + return copy.deepcopy(self) + + def replace(self, /, **kwargs) -> GenericMetadata: + tmp = self.copy() + tmp.__dict__.update(kwargs) + return tmp + def overlay(self, new_md: GenericMetadata) -> None: """Overlay a metadata object on this one @@ -408,7 +417,7 @@ md_test.credits = [ CreditMetadata({"primary": False, "person": "Sam Kieth", "role": "Cover"}), CreditMetadata({"primary": False, "person": "Ted Adams", "role": "Editor"}), ] -md_test.tags = [] +md_test.tags = set() md_test.pages = [ {"Image": 0, "ImageHeight": "1280", "ImageSize": "195977", "ImageWidth": "800", "Type": PageType.FrontCover}, {"Image": 1, "ImageHeight": "2039", "ImageSize": "611993", "ImageWidth": "1327"}, diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index d858e3a..34194c3 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -32,7 +32,7 @@ class CBLTransformer: # helper funcs def append_to_tags_if_unique(item: str) -> None: if item.casefold() not in (tag.casefold() for tag in self.metadata.tags): - self.metadata.tags.append(item) + self.metadata.tags.add(item) def add_string_list_to_tags(str_list: str | None) -> None: if str_list: diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py index 0d21203..f8662ff 100755 --- a/comictaggerlib/imagehasher.py +++ b/comictaggerlib/imagehasher.py @@ -49,7 +49,7 @@ class ImageHasher: def average_hash(self) -> int: try: - image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L") + image = self.image.resize((self.width, self.height), Image.Resampling.LANCZOS).convert("L") except Exception: logger.exception("average_hash error") return 0 diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 95a845b..fa64965 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -26,7 +26,7 @@ import pprint import re import sys import webbrowser -from typing import Any, Callable, cast +from typing import Any, Callable, Iterable, cast from urllib.parse import urlparse import natsort @@ -944,8 +944,8 @@ Have fun! tmp = self.teTags.toPlainText() if tmp is not None: - def strip_list(i: list[str]) -> list[str]: - return [x.strip() for x in i] + def strip_list(i: Iterable[str]) -> set[str]: + return {x.strip() for x in i} md.tags = strip_list(tmp.split(",")) diff --git a/testing/comicdata.py b/testing/comicdata.py index b4f20b6..9be6477 100644 --- a/testing/comicdata.py +++ b/testing/comicdata.py @@ -1,7 +1,5 @@ from __future__ import annotations -import dataclasses - import comicapi.genericmetadata import comictaggerlib.resulttypes from comicapi import utils @@ -55,15 +53,48 @@ select_details = [ metadata = [ ( comicapi.genericmetadata.GenericMetadata(series="test", issue="2", title="never"), - dataclasses.replace(comicapi.genericmetadata.md_test, series="test", issue="2", title="never"), + comicapi.genericmetadata.md_test.replace(series="test", issue="2", title="never"), ), ( comicapi.genericmetadata.GenericMetadata(series="", issue="2", title="never"), - dataclasses.replace(comicapi.genericmetadata.md_test, series=None, issue="2", title="never"), + comicapi.genericmetadata.md_test.replace(series=None, issue="2", title="never"), ), ( comicapi.genericmetadata.GenericMetadata(), - dataclasses.replace(comicapi.genericmetadata.md_test), + comicapi.genericmetadata.md_test.copy(), + ), +] + +metadata_keys = [ + ( + comicapi.genericmetadata.GenericMetadata(), + { + "issue_count": 6, + "issue_number": "1", + "month": 10, + "series": "Cory Doctorow's Futuristic Tales of the Here and Now", + "year": 2007, + }, + ), + ( + comicapi.genericmetadata.GenericMetadata(series="test"), + { + "issue_count": 6, + "issue_number": "1", + "month": 10, + "series": "test", + "year": 2007, + }, + ), + ( + comicapi.genericmetadata.GenericMetadata(series="test", issue="3"), + { + "issue_count": 6, + "issue_number": "3", + "month": 10, + "series": "test", + "year": 2007, + }, ), ] diff --git a/testing/comicvine.py b/testing/comicvine.py index 419bda4..c2ca298 100644 --- a/testing/comicvine.py +++ b/testing/comicvine.py @@ -22,16 +22,16 @@ cv_issue_result: dict[str, Any] = { "status_code": 1, "results": { "aliases": None, - "api_detail_url": "https://comicvine.gamespot.com/api/issue/4000-311811/", + "api_detail_url": "https://comicvine.gamespot.com/api/issue/4000-140529/", "associated_images": [], "character_credits": [], "character_died_in": [], "concept_credits": [], - "cover_date": "2007-12-01", - "date_added": "2012-01-18 06:48:56", - "date_last_updated": "2012-01-18 17:27:48", + "cover_date": "2007-10-01", + "date_added": "2008-10-16 05:25:47", + "date_last_updated": "2010-06-09 18:05:49", "deck": None, - "description": '

IDW Publishing continues to bring the stories of acclaimed science-fiction author Cory Doctorow to comics, this time offering up "Craphound." Despite the exoskeleton and mouth full of poisonous suckers, Jerry got along with the alien better than with most humans. In fact, Jerry had nicknamed him "Craphound", after their shared love of hunting for unique treasures at garage sales and thrift stores. They were buddies. That is, until Craphound found the old cowboy trunk. Adapted by Dara Naraghi (whose Lifelike debuts this month, too. See previous page for info), with a cover by Eisner-winning artist Paul Pope!

', + "description": "For 12-year-old Anda, getting paid real money to kill the characters of players who were cheating in her favorite online computer game was a win-win situation. Until she found out who was paying her, and what those characters meant to the livelihood of children around the world.", "first_appearance_characters": None, "first_appearance_concepts": None, "first_appearance_locations": None, @@ -39,22 +39,22 @@ cv_issue_result: dict[str, Any] = { "first_appearance_storyarcs": None, "first_appearance_teams": None, "has_staff_review": False, - "id": 311811, + "id": 140529, "image": { - "icon_url": "https://comicvine.gamespot.com/a/uploads/square_avatar/11/115179/2165551-cd3.jpg", - "medium_url": "https://comicvine.gamespot.com/a/uploads/scale_medium/11/115179/2165551-cd3.jpg", - "screen_url": "https://comicvine.gamespot.com/a/uploads/screen_medium/11/115179/2165551-cd3.jpg", - "screen_large_url": "https://comicvine.gamespot.com/a/uploads/screen_kubrick/11/115179/2165551-cd3.jpg", - "small_url": "https://comicvine.gamespot.com/a/uploads/scale_small/11/115179/2165551-cd3.jpg", - "super_url": "https://comicvine.gamespot.com/a/uploads/scale_large/11/115179/2165551-cd3.jpg", - "thumb_url": "https://comicvine.gamespot.com/a/uploads/scale_avatar/11/115179/2165551-cd3.jpg", - "tiny_url": "https://comicvine.gamespot.com/a/uploads/square_mini/11/115179/2165551-cd3.jpg", - "original_url": "https://comicvine.gamespot.com/a/uploads/original/11/115179/2165551-cd3.jpg", + "icon_url": "https://comicvine.gamespot.com/a/uploads/square_avatar/0/574/585444-109004_20080707014047_large.jpg", + "medium_url": "https://comicvine.gamespot.com/a/uploads/scale_medium/0/574/585444-109004_20080707014047_large.jpg", + "screen_url": "https://comicvine.gamespot.com/a/uploads/screen_medium/0/574/585444-109004_20080707014047_large.jpg", + "screen_large_url": "https://comicvine.gamespot.com/a/uploads/screen_kubrick/0/574/585444-109004_20080707014047_large.jpg", + "small_url": "https://comicvine.gamespot.com/a/uploads/scale_small/0/574/585444-109004_20080707014047_large.jpg", + "super_url": "https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg", + "thumb_url": "https://comicvine.gamespot.com/a/uploads/scale_avatar/0/574/585444-109004_20080707014047_large.jpg", + "tiny_url": "https://comicvine.gamespot.com/a/uploads/square_mini/0/574/585444-109004_20080707014047_large.jpg", + "original_url": "https://comicvine.gamespot.com/a/uploads/original/0/574/585444-109004_20080707014047_large.jpg", "image_tags": "All Images", }, - "issue_number": "3", + "issue_number": "1", "location_credits": [], - "name": "Craphound", + "name": "Anda's Game", "object_credits": [], "person_credits": [ { @@ -65,14 +65,35 @@ cv_issue_result: dict[str, Any] = { "role": "writer", }, { - "api_detail_url": "https://comicvine.gamespot.com/api/person/4040-4306/", - "id": 4306, - "name": "Paul Pope", - "site_detail_url": "https://comicvine.gamespot.com/paul-pope/4040-4306/", + "api_detail_url": "https://comicvine.gamespot.com/api/person/4040-57222/", + "id": 57222, + "name": "Esteve Polls", + "site_detail_url": "https://comicvine.gamespot.com/esteve-polls/4040-57222/", + "role": "artist", + }, + { + "api_detail_url": "https://comicvine.gamespot.com/api/person/4040-48472/", + "id": 48472, + "name": "Neil Uyetake", + "site_detail_url": "https://comicvine.gamespot.com/neil-uyetake/4040-48472/", + "role": "letterer", + }, + { + "api_detail_url": "https://comicvine.gamespot.com/api/person/4040-5329/", + "id": 5329, + "name": "Sam Kieth", + "site_detail_url": "https://comicvine.gamespot.com/sam-kieth/4040-5329/", "role": "cover", }, + { + "api_detail_url": "https://comicvine.gamespot.com/api/person/4040-58534/", + "id": 58534, + "name": "Ted Adams", + "site_detail_url": "https://comicvine.gamespot.com/ted-adams/4040-58534/", + "role": "editor", + }, ], - "site_detail_url": "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-311811/", + "site_detail_url": "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/", "store_date": None, "story_arc_credits": [], "team_credits": [], @@ -95,15 +116,33 @@ cv_volume_result: dict[str, Any] = { "number_of_total_results": 1, "status_code": 1, "results": { - "count_of_issues": 6, - "id": 23437, - "name": "Cory Doctorow's Futuristic Tales of the Here and Now", "aliases": None, + "api_detail_url": "https://comicvine.gamespot.com/api/volume/4050-23437/", + "count_of_issues": 6, + "date_added": "2008-10-16 05:25:47", + "date_last_updated": "2012-01-18 17:21:57", + "deck": None, + "description": "

Writer and BoingBoing.net co-editor Cory Doctorow has won acclaim for his science-fiction writing as well as his Creative Commons presentation of his material. Now, IDW Publishing is proud to present six standalone stories adapted from Doctorow's work, each featuring cover art by some of comics' top talents.

", + "id": 23437, + "image": { + "icon_url": "https://comicvine.gamespot.com/a/uploads/square_avatar/0/574/585444-109004_20080707014047_large.jpg", + "medium_url": "https://comicvine.gamespot.com/a/uploads/scale_medium/0/574/585444-109004_20080707014047_large.jpg", + "screen_url": "https://comicvine.gamespot.com/a/uploads/screen_medium/0/574/585444-109004_20080707014047_large.jpg", + "screen_large_url": "https://comicvine.gamespot.com/a/uploads/screen_kubrick/0/574/585444-109004_20080707014047_large.jpg", + "small_url": "https://comicvine.gamespot.com/a/uploads/scale_small/0/574/585444-109004_20080707014047_large.jpg", + "super_url": "https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg", + "thumb_url": "https://comicvine.gamespot.com/a/uploads/scale_avatar/0/574/585444-109004_20080707014047_large.jpg", + "tiny_url": "https://comicvine.gamespot.com/a/uploads/square_mini/0/574/585444-109004_20080707014047_large.jpg", + "original_url": "https://comicvine.gamespot.com/a/uploads/original/0/574/585444-109004_20080707014047_large.jpg", + "image_tags": "All Images", + }, + "name": "Cory Doctorow's Futuristic Tales of the Here and Now", "publisher": { "api_detail_url": "https://comicvine.gamespot.com/api/publisher/4010-1190/", "id": 1190, "name": "IDW Publishing", }, + "site_detail_url": "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4050-23437/", "start_year": "2007", }, "version": "1.0", @@ -143,7 +182,7 @@ cv_md = comicapi.genericmetadata.GenericMetadata( alternate_number=None, alternate_count=None, imprint=None, - notes="Tagged with ComicTagger 1.4.4a9.dev20 using info from Comic Vine on 2022-07-11 17:42:41. [Issue ID 311811]", + notes="Tagged with ComicTagger 1.4.4a9.dev20 using info from Comic Vine on 2022-07-11 17:42:41. [Issue ID 140529]", web_link=cv_issue_result["results"]["site_detail_url"], format=None, manga=None, @@ -157,18 +196,10 @@ cv_md = comicapi.genericmetadata.GenericMetadata( teams="", locations="", credits=[ - { - "person": cv_issue_result["results"]["person_credits"][0]["name"], - "role": cv_issue_result["results"]["person_credits"][0]["role"].title(), - "primary": False, - }, - { - "person": cv_issue_result["results"]["person_credits"][1]["name"], - "role": cv_issue_result["results"]["person_credits"][1]["role"].title(), - "primary": False, - }, + comicapi.genericmetadata.CreditMetadata(person=x["name"], role=x["role"].title(), primary=False) + for x in cv_issue_result["results"]["person_credits"] ], - tags=[], + tags=set(), pages=[], price=None, is_version_of=None, @@ -182,10 +213,10 @@ cv_md = comicapi.genericmetadata.GenericMetadata( class MockResponse: """Mocks the response object from requests""" - def __init__(self, result: dict[str, Any]) -> None: + def __init__(self, result: dict[str, Any], content=None) -> None: self.status_code = 200 self.result = result + self.content = content - # mock json() method always returns a specific testing dictionary def json(self) -> dict[str, list]: return self.result diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index 9c132aa..a57f655 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -6,7 +6,7 @@ import pytest import comicapi.comicarchive import comicapi.genericmetadata -from testing.filenames import cbz_path, datadir +from testing.filenames import datadir @pytest.mark.xfail(not comicapi.comicarchive.rar_support, reason="rar support") @@ -35,43 +35,31 @@ def test_metadata_read(cbz): assert md == comicapi.genericmetadata.md_test -def test_save_cix(tmp_path): - comic_path = tmp_path / cbz_path.name - shutil.copy(cbz_path, comic_path) +def test_save_cix(tmp_comic): + md = tmp_comic.read_cix() + md.set_default_page_list(tmp_comic.get_number_of_pages()) - c = comicapi.comicarchive.ComicArchive(comic_path) - md = c.read_cix() - md.set_default_page_list(c.get_number_of_pages()) + assert tmp_comic.write_cix(md) - assert c.write_cix(md) - - md = c.read_cix() + md = tmp_comic.read_cix() -def test_page_type_save(tmp_path): - comic_path = tmp_path / cbz_path.name - - shutil.copy(cbz_path, comic_path) - - c = comicapi.comicarchive.ComicArchive(comic_path) - md = c.read_cix() +def test_page_type_save(tmp_comic): + md = tmp_comic.read_cix() t = md.pages[0] t["Type"] = "" - assert c.write_cix(md) + assert tmp_comic.write_cix(md) - md = c.read_cix() + md = tmp_comic.read_cix() -def test_invalid_zip(tmp_path): - comic_path = tmp_path / cbz_path.name +def test_invalid_zip(tmp_comic): + with open(tmp_comic.path, mode="b+r") as f: + f.write(b"PK\000\000") - with open(cbz_path, mode="b+r") as f: - comic_path.write_bytes(b"PK\003\004" + f.read()[4:].replace(b"PK\003\004", b"PK\000\000")) - - c = comicapi.comicarchive.ComicArchive(comic_path) - - assert not c.write_cix(comicapi.genericmetadata.md_test) + result = tmp_comic.write_cix(comicapi.genericmetadata.md_test) + assert not result archivers = [ @@ -86,7 +74,7 @@ archivers = [ @pytest.mark.parametrize("archiver", archivers) -def test_copy_to_archive(archiver, tmp_path, cbz): +def test_copy_from_archive(archiver, tmp_path, cbz): comic_path = tmp_path / cbz.path.with_suffix("").name archive = archiver(comic_path) @@ -100,12 +88,3 @@ def test_copy_to_archive(archiver, tmp_path, cbz): md = comic_archive.read_cix() assert md == comicapi.genericmetadata.md_test - - md = comicapi.genericmetadata.GenericMetadata() - md.overlay(comicapi.genericmetadata.md_test) - md.series = "test" - - assert comic_archive.write_cix(md) - - test_md = comic_archive.read_cix() - assert md == test_md diff --git a/tests/comicvinetalker_test.py b/tests/comicvinetalker_test.py index b6e19c2..89cabba 100644 --- a/tests/comicvinetalker_test.py +++ b/tests/comicvinetalker_test.py @@ -8,71 +8,86 @@ import testing.comicvine from testing.comicdata import select_details -def test_fetch_volume_data(comicvine_api, settings, mock_now, comic_cache): +def test_search_for_series(comicvine_api, comic_cache): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - volume = ct.fetch_volume_data(23437) - volume["start_year"] = int(volume["start_year"]) - del volume["publisher"]["id"] - del volume["publisher"]["api_detail_url"] - assert volume == comic_cache.get_volume_info(23437, ct.source_name) + results = ct.search_for_series("cory doctorows futuristic tales of the here and now") + for r in results: + r["image"] = {"super_url": r["image"]["super_url"]} + r["start_year"] = int(r["start_year"]) + del r["publisher"]["id"] + del r["publisher"]["api_detail_url"] + cache_issues = comic_cache.get_search_results(ct.source_name, "cory doctorows futuristic tales of the here and now") + assert results == cache_issues -def test_fetch_issues_by_volume(comicvine_api, settings, comic_cache): +def test_fetch_volume_data(comicvine_api, comic_cache): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - issues = ct.fetch_issues_by_volume(23437) + result = ct.fetch_volume_data(23437) + result["start_year"] = int(result["start_year"]) + del result["publisher"]["id"] + del result["publisher"]["api_detail_url"] + assert result == comic_cache.get_volume_info(23437, ct.source_name) + + +def test_fetch_issues_by_volume(comicvine_api, comic_cache): + ct = comictaggerlib.comicvinetalker.ComicVineTalker() + results = ct.fetch_issues_by_volume(23437) cache_issues = comic_cache.get_volume_issues_info(23437, ct.source_name) - - issues[0]["image"] = {"super_url": issues[0]["image"]["super_url"], "thumb_url": issues[0]["image"]["thumb_url"]} - del issues[0]["volume"] - assert issues == cache_issues + for r in results: + r["image"] = {"super_url": r["image"]["super_url"], "thumb_url": r["image"]["thumb_url"]} + del r["volume"] + assert results == cache_issues def test_fetch_issue_data_by_issue_id(comicvine_api, settings, mock_now, mock_version): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - md = ct.fetch_issue_data_by_issue_id(311811, settings) - assert md == testing.comicvine.cv_md + result = ct.fetch_issue_data_by_issue_id(140529, settings) + assert result == testing.comicvine.cv_md def test_fetch_issues_by_volume_issue_num_and_year(comicvine_api): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - cv = ct.fetch_issues_by_volume_issue_num_and_year([23437], "3", None) + results = ct.fetch_issues_by_volume_issue_num_and_year([23437], "1", None) cv_expected = testing.comicvine.cv_issue_result["results"].copy() testing.comicvine.filter_field_list( cv_expected, {"params": {"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description,aliases"}}, ) - assert cv[0] == cv_expected + for r, e in zip(results, [cv_expected]): + assert r == e cv_issue = [ - (23437, "3", testing.comicvine.cv_md), - (23437, "", comicapi.genericmetadata.GenericMetadata()), + (23437, "", testing.comicvine.cv_md), + (23437, "1", testing.comicvine.cv_md), (23437, "0", comicapi.genericmetadata.GenericMetadata()), ] -@pytest.mark.parametrize("volume_id, issue_number, result_md", cv_issue) -def test_fetch_issue_data(comicvine_api, settings, mock_now, mock_version, volume_id, issue_number, result_md): +@pytest.mark.parametrize("volume_id, issue_number, expected", cv_issue) +def test_fetch_issue_data(comicvine_api, settings, mock_now, mock_version, volume_id, issue_number, expected): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - md = ct.fetch_issue_data(volume_id, issue_number, settings) - assert md == result_md + results = ct.fetch_issue_data(volume_id, issue_number, settings) + assert results == expected -# @pytest.mark.parametrize("volume_id, issue_number, result_md", cv_issue) -def test_fetch_issue_select_details(comicvine_api, settings, mock_now, mock_version): +def test_fetch_issue_select_details(comicvine_api, mock_now, mock_version): ct = comictaggerlib.comicvinetalker.ComicVineTalker() - md = ct.fetch_issue_select_details(311811) - res = { + result = ct.fetch_issue_select_details(140529) + expected = { "cover_date": testing.comicvine.cv_issue_result["results"]["cover_date"], "site_detail_url": testing.comicvine.cv_issue_result["results"]["site_detail_url"], "image_url": testing.comicvine.cv_issue_result["results"]["image"]["super_url"], "thumb_image_url": testing.comicvine.cv_issue_result["results"]["image"]["thumb_url"], } - assert md == res + assert result == expected @pytest.mark.parametrize("details", select_details) def test_issue_select_details(comic_cache, details): + expected = details.copy() + del expected["issue_id"] + ct = comictaggerlib.comicvinetalker.ComicVineTalker() ct.cache_issue_select_details( issue_id=details["issue_id"], @@ -81,6 +96,6 @@ def test_issue_select_details(comic_cache, details): cover_date=details["cover_date"], page_url=details["site_detail_url"], ) - det = details.copy() - del det["issue_id"] - assert det == comic_cache.get_issue_select_details(details["issue_id"], ct.source_name) + result = comic_cache.get_issue_select_details(details["issue_id"], ct.source_name) + + assert result == expected diff --git a/tests/conftest.py b/tests/conftest.py index 6c03d55..654357a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,15 @@ from __future__ import annotations -import dataclasses +import copy import datetime +import io +import shutil import unittest.mock from typing import Any, Generator import pytest import requests +from PIL import Image import comicapi.comicarchive import comicapi.genericmetadata @@ -23,6 +26,27 @@ def cbz(): yield comicapi.comicarchive.ComicArchive(filenames.cbz_path) +@pytest.fixture +def tmp_comic(tmp_path): + shutil.copy(filenames.cbz_path, tmp_path) + yield comicapi.comicarchive.ComicArchive(tmp_path / filenames.cbz_path.name) + + +@pytest.fixture +def cbz_double_cover(tmp_path, tmp_comic): + + cover = Image.open(io.BytesIO(tmp_comic.get_page(0))) + + other_page = Image.open(io.BytesIO(tmp_comic.get_page(tmp_comic.get_number_of_pages() - 1))) + + double_cover = Image.new("RGB", (cover.width * 2, cover.height)) + double_cover.paste(other_page, (0, 0)) + double_cover.paste(cover, (cover.width, 0)) + + tmp_comic.archiver.write_file("double_cover.jpg", double_cover.tobytes("jpeg", "RGB")) + yield tmp_comic + + @pytest.fixture(autouse=True) def no_requests(monkeypatch) -> None: """Remove requests.sessions.Session.request for all tests.""" @@ -30,16 +54,24 @@ def no_requests(monkeypatch) -> None: @pytest.fixture -def comicvine_api(monkeypatch) -> unittest.mock.Mock: +def comicvine_api(monkeypatch, cbz, comic_cache) -> unittest.mock.Mock: # 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. + def make_list(cv_result): + cv_list = copy.deepcopy(cv_result) + if isinstance(cv_list["results"], dict): + cv_list["results"] = [cv_list["results"]] + return cv_list + def mock_get(*args, **kwargs): if args: if args[0].startswith("https://comicvine.gamespot.com/api/volume/4050-23437"): - return comicvine.MockResponse(comicvine.cv_volume_result) - if args[0].startswith("https://comicvine.gamespot.com/api/issue/4000-311811"): + cv_result = copy.deepcopy(comicvine.cv_volume_result) + comicvine.filter_field_list(cv_result["results"], kwargs) + return comicvine.MockResponse(cv_result) + if args[0].startswith("https://comicvine.gamespot.com/api/issue/4000-140529"): return comicvine.MockResponse(comicvine.cv_issue_result) if ( args[0].startswith("https://comicvine.gamespot.com/api/issues/") @@ -47,11 +79,32 @@ def comicvine_api(monkeypatch) -> unittest.mock.Mock: and "filter" in kwargs["params"] and "23437" in kwargs["params"]["filter"] ): - cv_list = comicvine.cv_issue_result.copy() - cv_list["results"] = cv_list["results"].copy() - comicvine.filter_field_list(cv_list["results"], kwargs) - cv_list["results"] = [cv_list["results"]] + cv_list = make_list(comicvine.cv_issue_result) + for cv in cv_list["results"]: + comicvine.filter_field_list(cv, kwargs) return comicvine.MockResponse(cv_list) + if ( + args[0].startswith("https://comicvine.gamespot.com/api/search") + and "params" in kwargs + and "resources" in kwargs["params"] + and "volume" == kwargs["params"]["resources"] + ): + cv_list = make_list(comicvine.cv_volume_result) + for cv in cv_list["results"]: + comicvine.filter_field_list(cv, kwargs) + return comicvine.MockResponse(cv_list) + if ( + args[0] + == "https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg" + ): + return comicvine.MockResponse({}, cbz.get_page(0)) + if ( + args[0] + == "https://comicvine.gamespot.com/a/uploads/scale_avatar/0/574/585444-109004_20080707014047_large.jpg" + ): + thumb = Image.open(io.BytesIO(cbz.get_page(0))) + thumb.resize((105, 160), Image.Resampling.LANCZOS) + return comicvine.MockResponse({}, thumb.tobytes("jpeg", "RGB")) return comicvine.MockResponse(comicvine.cv_not_found) m_get = unittest.mock.Mock(side_effect=mock_get) @@ -86,7 +139,7 @@ def mock_version(monkeypatch): @pytest.fixture def md(): - yield dataclasses.replace(comicapi.genericmetadata.md_test) + yield comicapi.genericmetadata.md_test.copy() # manually seeds publishers diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py index e753447..c10574b 100644 --- a/tests/genericmetadata_test.py +++ b/tests/genericmetadata_test.py @@ -26,7 +26,7 @@ def test_add_credit(): md = comicapi.genericmetadata.GenericMetadata() md.add_credit(person="test", role="writer", primary=False) - md.credits == [{"person": "test", "role": "writer", "primary": False}] + assert md.credits == [comicapi.genericmetadata.CreditMetadata(person="test", role="writer", primary=False)] def test_add_credit_primary(): @@ -34,7 +34,7 @@ def test_add_credit_primary(): md.add_credit(person="test", role="writer", primary=False) md.add_credit(person="test", role="writer", primary=True) - md.credits == [{"person": "test", "role": "writer", "primary": True}] + assert md.credits == [comicapi.genericmetadata.CreditMetadata(person="test", role="writer", primary=True)] @pytest.mark.parametrize("role, expected", credits) diff --git a/tests/issueidentifier_test.py b/tests/issueidentifier_test.py new file mode 100644 index 0000000..2111839 --- /dev/null +++ b/tests/issueidentifier_test.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import pytest + +import comicapi.comicarchive +import comicapi.issuestring +import comictaggerlib.comicvinetalker +import comictaggerlib.issueidentifier +import testing.comicdata +import testing.comicvine + + +def test_crop(cbz_double_cover, settings, tmp_path): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz_double_cover, settings) + cropped = ii.crop_cover(cbz_double_cover.archiver.read_file("double_cover.jpg")) + original_cover = cbz_double_cover.get_page(0) + + original_hash = ii.calculate_hash(original_cover) + cropped_hash = ii.calculate_hash(cropped) + + assert original_hash == cropped_hash + + +@pytest.mark.parametrize("additional_md, expected", testing.comicdata.metadata_keys) +def test_get_search_keys(cbz, settings, additional_md, expected): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings) + 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) + score = ii.get_issue_cover_match_score( + comictaggerlib.comicvinetalker.ComicVineTalker(), + int( + comicapi.issuestring.IssueString( + cbz.read_metadata(comicapi.comicarchive.MetaDataStyle.CIX).issue + ).as_float() + ), + "https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg", + "https://comicvine.gamespot.com/a/uploads/scale_avatar/0/574/585444-109004_20080707014047_large.jpg", + "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/", + [ii.calculate_hash(cbz.get_page(0))], + ) + expected = { + "hash": 1747255366011518976, + "score": 0, + "url": "https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg", + } + assert expected == score + + +def test_search(cbz, settings, comicvine_api): + ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings) + results = ii.search() + cv_expected = { + "series": f"{testing.comicvine.cv_volume_result['results']['name']} ({testing.comicvine.cv_volume_result['results']['start_year']})", + "distance": 0, + "issue_number": testing.comicvine.cv_issue_result["results"]["issue_number"], + "cv_issue_count": testing.comicvine.cv_volume_result["results"]["count_of_issues"], + "issue_title": testing.comicvine.cv_issue_result["results"]["name"], + "issue_id": testing.comicvine.cv_issue_result["results"]["id"], + "volume_id": testing.comicvine.cv_volume_result["results"]["id"], + "month": testing.comicvine.date[1], + "year": testing.comicvine.date[2], + "publisher": testing.comicvine.cv_volume_result["results"]["publisher"]["name"], + "image_url": testing.comicvine.cv_issue_result["results"]["image"]["super_url"], + "thumb_url": testing.comicvine.cv_issue_result["results"]["image"]["thumb_url"], + "page_url": testing.comicvine.cv_issue_result["results"]["site_detail_url"], + "description": testing.comicvine.cv_issue_result["results"]["description"], + } + for r, e in zip(results, [cv_expected]): + del r["url_image_hash"] + assert r == e