diff --git a/.flake8 b/.flake8 index 6193c99..0aca4d8 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 120 -extend-ignore = E203, E501, E722 +extend-ignore = E203, E501, A003 extend-exclude = venv, scripts, build, dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..551c572 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: name-tests-test + - id: requirements-txt-fixer +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.1 + hooks: + - id: setup-cfg-fmt +- repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + args: [--af,--add-import, 'from __future__ import annotations'] +- repo: https://github.com/asottile/pyupgrade + rev: v2.32.1 + hooks: + - id: pyupgrade + args: [--py39-plus] +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black +- repo: https://github.com/PyCQA/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: [-i] +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-eradicate, flake8-length] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.960 + hooks: + - id: mypy + additional_dependencies: [types-setuptools, types-requests] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e8a1d8..9447b1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,4 +134,4 @@ tests/test_comicarchive.py x... [ 73%] tests/test_rename.py ..xxx.xx..XXX.XX [100%] ================== 27 passed, 29 xfailed, 5 xpassed in 2.68s =================== -``` \ No newline at end of file +``` diff --git a/comicapi/__init__.py b/comicapi/__init__.py index 06d5141..cc3ff07 100644 --- a/comicapi/__init__.py +++ b/comicapi/__init__.py @@ -1 +1,3 @@ +from __future__ import annotations + __author__ = "dromanin" diff --git a/comicapi/comet.py b/comicapi/comet.py index 686cc2e..d8da089 100644 --- a/comicapi/comet.py +++ b/comicapi/comet.py @@ -1,18 +1,19 @@ """A class to encapsulate CoMet data""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import xml.etree.ElementTree as ET @@ -208,7 +209,7 @@ class CoMet: root = tree.getroot() if root.tag != "comet": raise Exception - except: + except ET.ParseError: return False return True diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 351647e..9d36f4d 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -1,18 +1,18 @@ """A class to represent a single comic, be it file or folder of images""" - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import io import logging @@ -21,20 +21,26 @@ import pathlib import platform import struct import subprocess -import sys import tempfile import time import zipfile +from typing import cast import natsort import py7zr import wordninja +from comicapi import filenamelexer, filenameparser, utils +from comicapi.comet import CoMet +from comicapi.comicbookinfo import ComicBookInfo +from comicapi.comicinfoxml import ComicInfoXml +from comicapi.genericmetadata import GenericMetadata, PageType + try: from unrar.cffi import rarfile rar_support = True -except: +except ImportError: rar_support = False try: @@ -44,20 +50,11 @@ try: except ImportError: pil_available = False -from typing import List, Optional, Union, cast - -from comicapi import filenamelexer, filenameparser, utils -from comicapi.comet import CoMet -from comicapi.comicbookinfo import ComicBookInfo -from comicapi.comicinfoxml import ComicInfoXml -from comicapi.genericmetadata import GenericMetadata, PageType logger = logging.getLogger(__name__) if not pil_available: logger.exception("PIL unavalable") -sys.path.insert(0, os.path.abspath(".")) - class MetaDataStyle: CBI = 0 @@ -71,7 +68,7 @@ class UnknownArchiver: """Unknown implementation""" - def __init__(self, path: Union[pathlib.Path, str]) -> None: + def __init__(self, path: pathlib.Path | str) -> None: self.path = path def get_comment(self) -> str: @@ -80,7 +77,7 @@ class UnknownArchiver: def set_comment(self, comment: str) -> bool: return False - def read_file(self, archive_file: str) -> Optional[bytes]: + def read_file(self, archive_file: str) -> bytes | None: return None def write_file(self, archive_file: str, data: bytes) -> bool: @@ -97,7 +94,7 @@ class SevenZipArchiver(UnknownArchiver): """7Z implementation""" - def __init__(self, path: Union[pathlib.Path, str]) -> None: + def __init__(self, path: pathlib.Path | str) -> None: self.path = pathlib.Path(path) # @todo: Implement Comment? @@ -114,17 +111,17 @@ class SevenZipArchiver(UnknownArchiver): data = zf.read(archive_file)[archive_file].read() except py7zr.Bad7zFile as e: logger.error("bad 7zip file [%s]: %s :: %s", e, self.path, archive_file) - raise IOError from e - except Exception as e: + raise OSError from e + except OSError as e: logger.error("bad 7zip file [%s]: %s :: %s", e, self.path, archive_file) - raise IOError from e + raise OSError from e return data def remove_file(self, archive_file: str) -> bool: try: self.rebuild_zip_file([archive_file]) - except: + except (py7zr.Bad7zFile, OSError): logger.exception("Failed to remove %s from 7zip archive", archive_file) return False else: @@ -143,7 +140,7 @@ class SevenZipArchiver(UnknownArchiver): with py7zr.SevenZipFile(self.path, "a") as zf: zf.writestr(data, archive_file) return True - except: + except (py7zr.Bad7zFile, OSError): logger.exception("Writing zip file failed") return False @@ -153,7 +150,7 @@ class SevenZipArchiver(UnknownArchiver): namelist: list[str] = zf.getnames() return namelist - except Exception as e: + except (py7zr.Bad7zFile, OSError) as e: logger.error("Unable to get 7zip file list [%s]: %s", e, self.path) return [] @@ -172,7 +169,7 @@ class SevenZipArchiver(UnknownArchiver): with py7zr.SevenZipFile(tmp_name, "w") as zout: for fname, bio in zin.read(targets).items(): zout.writef(bio, fname) - except Exception: + except (py7zr.Bad7zFile, OSError): logger.exception("Error rebuilding 7zip file: %s", self.path) # replace with the new file @@ -198,7 +195,7 @@ class ZipArchiver(UnknownArchiver): """ZIP implementation""" - def __init__(self, path: Union[pathlib.Path, str]) -> None: + def __init__(self, path: pathlib.Path | str) -> None: self.path = pathlib.Path(path) def get_comment(self) -> str: @@ -217,16 +214,16 @@ class ZipArchiver(UnknownArchiver): data = zf.read(archive_file) except zipfile.BadZipfile as e: logger.error("bad zipfile [%s]: %s :: %s", e, self.path, archive_file) - raise IOError from e - except Exception as e: + raise OSError from e + except OSError as e: logger.error("bad zipfile [%s]: %s :: %s", e, self.path, archive_file) - raise IOError from e + raise OSError from e return data def remove_file(self, archive_file: str) -> bool: try: self.rebuild_zip_file([archive_file]) - except: + except (zipfile.BadZipfile, OSError): logger.exception("Failed to remove %s from zip archive", archive_file) return False else: @@ -245,20 +242,20 @@ class ZipArchiver(UnknownArchiver): with zipfile.ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf: zf.writestr(archive_file, data) return True - except Exception as e: + except (zipfile.BadZipfile, OSError) as e: logger.error("writing zip file failed [%s]: %s", e, self.path) return False - def get_filename_list(self) -> List[str]: + def get_filename_list(self) -> list[str]: try: with zipfile.ZipFile(self.path, "r") as zf: namelist = zf.namelist() return namelist - except Exception as e: + except (zipfile.BadZipfile, OSError) as e: logger.error("Unable to get zipfile list [%s]: %s", e, self.path) return [] - def rebuild_zip_file(self, exclude_list: List[str]) -> None: + def rebuild_zip_file(self, exclude_list: list[str]) -> None: """Zip helper func This recompresses the zip archive, without the files in the exclude_list @@ -276,14 +273,14 @@ class ZipArchiver(UnknownArchiver): # preserve the old comment zout.comment = zin.comment - except Exception: + except (zipfile.BadZipfile, OSError): logger.exception("Error rebuilding zip file: %s", self.path) # replace with the new file os.remove(self.path) os.rename(tmp_name, self.path) - def write_zip_comment(self, filename: Union[pathlib.Path, str], comment: str) -> bool: + def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool: """ This is a custom function for writing a comment to a zip file, since the built-in one doesn't seem to work on Windows and Mac OS/X @@ -370,7 +367,7 @@ class RarArchiver(UnknownArchiver): devnull = None - def __init__(self, path: Union[pathlib.Path, str], rar_exe_path: str) -> None: + def __init__(self, path: pathlib.Path | str, rar_exe_path: str) -> None: self.path = pathlib.Path(path) self.rar_exe_path = rar_exe_path @@ -411,7 +408,7 @@ class RarArchiver(UnknownArchiver): if platform.system() == "Darwin": time.sleep(1) os.remove(tmp_name) - except Exception: + except (subprocess.CalledProcessError, OSError): logger.exception("Failed to set a comment") return False else: @@ -442,7 +439,7 @@ class RarArchiver(UnknownArchiver): tries, ) continue - except (OSError, IOError) as e: + except OSError as e: logger.error("read_file(): [%s] %s:%s attempt #%d", e, self.path, archive_file, tries) time.sleep(1) except Exception as e: @@ -458,9 +455,9 @@ class RarArchiver(UnknownArchiver): if len(entries) == 1: return entries[0][1] - raise IOError + raise OSError - raise IOError + raise OSError def write_file(self, archive_file: str, data: bytes) -> bool: @@ -490,7 +487,7 @@ class RarArchiver(UnknownArchiver): time.sleep(1) os.remove(tmp_file) os.rmdir(tmp_folder) - except Exception as e: + except (subprocess.CalledProcessError, OSError) as e: logger.info(str(e)) logger.exception("Failed write %s to rar archive", archive_file) return False @@ -513,7 +510,7 @@ class RarArchiver(UnknownArchiver): if platform.system() == "Darwin": time.sleep(1) - except: + except (subprocess.CalledProcessError, OSError): logger.exception("Failed to remove %s from rar archive", archive_file) return False else: @@ -532,16 +529,16 @@ class RarArchiver(UnknownArchiver): if item.file_size != 0: namelist.append(item.filename) - except (OSError, IOError) as e: + except OSError as e: logger.error(f"get_filename_list(): [{e}] {self.path} attempt #{tries}".format(str(e), self.path, tries)) time.sleep(1) return namelist - def get_rar_obj(self) -> "Optional[rarfile.RarFile]": + def get_rar_obj(self) -> rarfile.RarFile | None: try: rarc = rarfile.RarFile(str(self.path)) - except (OSError, IOError) as e: + except OSError as e: logger.error("getRARObj(): [%s] %s", e, self.path) else: return rarc @@ -553,7 +550,7 @@ class FolderArchiver(UnknownArchiver): """Folder implementation""" - def __init__(self, path: Union[pathlib.Path, str]) -> None: + def __init__(self, path: pathlib.Path | str) -> None: self.path = pathlib.Path(path) self.comment_file_name = "ComicTaggerFolderComment.txt" @@ -570,7 +567,7 @@ class FolderArchiver(UnknownArchiver): try: with open(fname, "rb") as f: data = f.read() - except IOError: + except OSError: logger.exception("Failed to read: %s", fname) return data @@ -581,7 +578,7 @@ class FolderArchiver(UnknownArchiver): try: with open(fname, "wb") as f: f.write(data) - except: + except OSError: logger.exception("Failed to write: %s", fname) return False else: @@ -592,7 +589,7 @@ class FolderArchiver(UnknownArchiver): fname = os.path.join(self.path, archive_file) try: os.remove(fname) - except: + except OSError: logger.exception("Failed to remove: %s", fname) return False else: @@ -601,7 +598,7 @@ class FolderArchiver(UnknownArchiver): def get_filename_list(self) -> list[str]: return self.list_files(self.path) - def list_files(self, folder: Union[pathlib.Path, str]) -> list[str]: + def list_files(self, folder: pathlib.Path | str) -> list[str]: itemlist = [] @@ -621,19 +618,19 @@ class ComicArchive: def __init__( self, - path: Union[pathlib.Path, str], + path: pathlib.Path | str, rar_exe_path: str = "", - default_image_path: Union[pathlib.Path, str, None] = None, + default_image_path: pathlib.Path | str | None = None, ) -> None: - self.cbi_md: Optional[GenericMetadata] = None - self.cix_md: Optional[GenericMetadata] = None - self.comet_filename: Optional[str] = None - self.comet_md: Optional[GenericMetadata] = None - self.has__cbi: Optional[bool] = None - self.has__cix: Optional[bool] = None - self.has__comet: Optional[bool] = None + self.cbi_md: GenericMetadata | None = None + self.cix_md: GenericMetadata | None = None + self.comet_filename: str | None = None + self.comet_md: GenericMetadata | None = None + self.has__cbi: bool | None = None + self.has__cix: bool | None = None + self.has__comet: bool | None = None self.path = pathlib.Path(path) - self.page_count: Optional[int] = None + self.page_count: int | None = None self.page_list: list[str] = [] self.rar_exe_path = rar_exe_path @@ -688,11 +685,11 @@ class ComicArchive: self.cbi_md = None self.comet_md = None - def load_cache(self, style_list: List[int]) -> None: + def load_cache(self, style_list: list[int]) -> None: for style in style_list: self.read_metadata(style) - def rename(self, path: Union[pathlib.Path, str]) -> None: + def rename(self, path: pathlib.Path | str) -> None: self.path = pathlib.Path(path) self.archiver.path = pathlib.Path(path) @@ -703,10 +700,9 @@ class ComicArchive: return zipfile.is_zipfile(self.path) def rar_test(self) -> bool: - try: - return bool(rarfile.is_rarfile(str(self.path))) - except: - return False + if rar_support: + return rarfile.is_rarfile(str(self.path)) + return False def is_sevenzip(self) -> bool: return self.archive_type == self.ArchiveType.SevenZip @@ -798,7 +794,7 @@ class ComicArchive: if filename: try: image_data = self.archiver.read_file(filename) or bytes() - except IOError: + except OSError: logger.exception("Error reading in page. Substituting logo page.") image_data = ComicArchive.logo_data @@ -816,7 +812,7 @@ class ComicArchive: return page_list[index] - def get_scanner_page_index(self) -> Optional[int]: + def get_scanner_page_index(self) -> int | None: scanner_page_index = None # make a guess at the scanner page @@ -864,7 +860,7 @@ class ComicArchive: return scanner_page_index - def get_page_name_list(self, sort_list: bool = True) -> List[str]: + def get_page_name_list(self, sort_list: bool = True) -> list[str]: if not self.page_list: # get the list file names in the archive, and sort files: list[str] = self.archiver.get_filename_list() @@ -966,7 +962,7 @@ class ComicArchive: return b"" try: raw_cix = self.archiver.read_file(self.ci_xml_filename) or b"" - except IOError as e: + except OSError as e: logger.error("Error reading in raw CIX!: %s", e) raw_cix = bytes() return raw_cix @@ -1035,13 +1031,13 @@ class ComicArchive: if not self.has_comet(): logger.info("%s doesn't have CoMet data!", self.path) raw_comet = "" - - try: - raw_bytes = self.archiver.read_file(cast(str, self.comet_filename)) - if raw_bytes: - raw_comet = raw_bytes.decode("utf-8") - except: - logger.exception("Error reading in raw CoMet!") + else: + try: + raw_bytes = self.archiver.read_file(cast(str, self.comet_filename)) + if raw_bytes: + raw_comet = raw_bytes.decode("utf-8") + except OSError as e: + logger.exception("Error reading in raw CoMet!: %s", e) return raw_comet def write_comet(self, metadata: GenericMetadata) -> bool: diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py index 6bdcebb..b2cd35a 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -1,24 +1,24 @@ """A class to encapsulate the ComicBookInfo data""" - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import json import logging from collections import defaultdict from datetime import datetime -from typing import Any, Literal, TypedDict, Union +from typing import Any, Literal, TypedDict from comicapi import utils from comicapi.genericmetadata import GenericMetadata @@ -119,12 +119,12 @@ class ComicBookInfo: cbi_container = self.create_json_dictionary(metadata) return json.dumps(cbi_container) - def validate_string(self, string: Union[bytes, str]) -> bool: + def validate_string(self, string: bytes | str) -> bool: """Verify that the string actually contains CBI data in JSON format""" try: cbi_container = json.loads(string) - except: + except json.JSONDecodeError: return False return "ComicBookInfo/1.0" in cbi_container diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py index fd619c2..271c966 100644 --- a/comicapi/comicinfoxml.py +++ b/comicapi/comicinfoxml.py @@ -1,23 +1,23 @@ """A class to encapsulate ComicRack's ComicInfo.xml data""" - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import xml.etree.ElementTree as ET from collections import OrderedDict -from typing import Any, List, Optional, cast +from typing import Any, cast from xml.etree.ElementTree import ElementTree from comicapi import utils @@ -37,7 +37,7 @@ class ComicInfoXml: cover_synonyms = ["cover", "covers", "coverartist", "cover artist"] editor_synonyms = ["editor"] - def get_parseable_credits(self) -> List[str]: + def get_parseable_credits(self) -> list[str]: parsable_credits = [] parsable_credits.extend(self.writer_synonyms) parsable_credits.extend(self.penciller_synonyms) @@ -59,7 +59,7 @@ class ComicInfoXml: return str(tree_str) def convert_metadata_to_xml( - self, filename: "ComicInfoXml", metadata: GenericMetadata, xml: bytes = b"" + self, filename: ComicInfoXml, metadata: GenericMetadata, xml: bytes = b"" ) -> ElementTree: # shorthand for the metadata @@ -192,7 +192,7 @@ class ComicInfoXml: if root.tag != "ComicInfo": raise Exception("Not a ComicInfo file") - def get(name: str) -> Optional[str]: + def get(name: str) -> str | None: tag = root.find(name) if tag is None: return None diff --git a/comicapi/data/publishers.json b/comicapi/data/publishers.json index e80915b..9e0a1a1 100644 --- a/comicapi/data/publishers.json +++ b/comicapi/data/publishers.json @@ -127,4 +127,4 @@ "red circle Comics": "Dark Circle Comics", "red circle": "Dark Circle Comics" } -} \ No newline at end of file +} diff --git a/comicapi/filenamelexer.py b/comicapi/filenamelexer.py index 7fce422..0fbd329 100644 --- a/comicapi/filenamelexer.py +++ b/comicapi/filenamelexer.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import calendar import os import unicodedata from enum import Enum, auto -from typing import Any, Callable, Optional, Set +from typing import Any, Callable class ItemType(Enum): @@ -86,7 +88,7 @@ class Item: class Lexer: def __init__(self, string: str) -> None: self.input: str = string # The string being scanned - self.state: Optional[Callable[[Lexer], Optional[Callable]]] = None # The next lexing function to enter + self.state: Callable[[Lexer], Callable | None] | None = None # The next lexing function to enter self.pos: int = -1 # Current position in the input self.start: int = 0 # Start position of this item self.lastPos: int = 0 # Position of most recent item returned by nextItem @@ -168,13 +170,13 @@ class Lexer: # Errorf returns an error token and terminates the scan by passing # Back a nil pointer that will be the next state, terminating self.nextItem. -def errorf(lex: Lexer, message: str) -> Optional[Callable[[Lexer], Optional[Callable]]]: +def errorf(lex: Lexer, message: str) -> Callable[[Lexer], Callable | None] | None: lex.items.append(Item(ItemType.Error, lex.start, message)) return None # Scans the elements inside action delimiters. -def lex_filename(lex: Lexer) -> Optional[Callable[[Lexer], Optional[Callable]]]: +def lex_filename(lex: Lexer) -> Callable[[Lexer], Callable | None] | None: r = lex.get() if r == eof: if lex.paren_depth != 0: @@ -301,7 +303,7 @@ def lex_text(lex: Lexer) -> Callable: return lex_filename -def cal(value: str) -> Set[Any]: +def cal(value: str) -> set[Any]: month_abbr = [i for i, x in enumerate(calendar.month_abbr) if x == value.title()] month_name = [i for i, x in enumerate(calendar.month_name) if x == value.title()] day_abbr = [i for i, x in enumerate(calendar.day_abbr) if x == value.title()] @@ -309,7 +311,7 @@ def cal(value: str) -> Set[Any]: return set(month_abbr + month_name + day_abbr + day_name) -def lex_number(lex: Lexer) -> Optional[Callable[[Lexer], Optional[Callable]]]: +def lex_number(lex: Lexer) -> Callable[[Lexer], Callable | None] | None: if not lex.scan_number(): return errorf(lex, "bad number syntax: " + lex.input[lex.start : lex.pos]) # Complex number logic removed. Messes with math operations without space diff --git a/comicapi/filenameparser.py b/comicapi/filenameparser.py index 54c3e9b..9050e54 100644 --- a/comicapi/filenameparser.py +++ b/comicapi/filenameparser.py @@ -2,29 +2,29 @@ This should probably be re-written, but, well, it mostly works! """ - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +# # Some portions of this code were modified from pyComicMetaThis project # http://code.google.com/p/pycomicmetathis/ +from __future__ import annotations import logging import os import re from operator import itemgetter -from typing import Callable, Match, Optional, TypedDict +from typing import Callable, Match, TypedDict from urllib.parse import unquote from text2digits import text2digits @@ -58,7 +58,7 @@ class FileNameParser: placeholders = [r"[_]", r" +"] for ph in placeholders: string = re.sub(ph, self.repl, string) - return string # .strip() + return string def get_issue_count(self, filename: str, issue_end: int) -> str: @@ -176,13 +176,11 @@ class FileNameParser: # in case there is no issue number, remove some obvious stuff if "--" in filename: - # the pattern seems to be that anything to left of the first "--" - # is the series name followed by issue + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue filename = re.sub(r"--.*", self.repl, filename) elif "__" in filename: - # the pattern seems to be that anything to left of the first "__" - # is the series name followed by issue + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue filename = re.sub(r"__.*", self.repl, filename) filename = filename.replace("+", " ") @@ -192,9 +190,10 @@ class FileNameParser: volume = "" # save the last word - try: - last_word = series.split()[-1] - except: + split = series.split() + if split: + last_word = split[-1] + else: last_word = "" # remove any parenthetical phrases @@ -223,12 +222,11 @@ class FileNameParser: # be removed to help search online if issue_start == 0: one_shot_words = ["tpb", "os", "one-shot", "ogn", "gn"] - try: - last_word = series.split()[-1] - if last_word.lower() in one_shot_words: - series = series.rsplit(" ", 1)[0] - except: - pass + split = series.split() + if split: + last_word = split[-1] + if last_word.casefold() in one_shot_words: + series, _, _ = series.rpartition(" ") if volume: series = re.sub(r"\s+v(|ol|olume)$", "", series) @@ -343,7 +341,7 @@ class Parser: remove_fcbd: bool = False, remove_publisher: bool = False, ) -> None: - self.state: Optional[Callable[[Parser], Optional[Callable]]] = None + self.state: Callable[[Parser], Callable | None] | None = None self.pos = -1 self.firstItem = True @@ -406,7 +404,7 @@ class Parser: self.state = self.state(self) -def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: +def parse(p: Parser) -> Callable[[Parser], Callable | None] | None: item: filenamelexer.Item = p.get() # We're done, time to do final processing @@ -430,7 +428,8 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: if len(item.val.lstrip("0")) < 4: # An issue number starting with # Was not found and no previous number was found if p.issue_number_at is None: - # Series has already been started/parsed, filters out leading alternate numbers leading alternate number + # Series has already been started/parsed, + # filters out leading alternate numbers leading alternate number if len(p.series_parts) > 0: # Unset first item if p.firstItem: @@ -526,7 +525,8 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: if p.peek_back().typ == filenamelexer.ItemType.Dot: p.used_items.append(p.peek_back()) - # Allows removing DC from 'Wonder Woman 49 DC Sep-Oct 1951' dependent on publisher being in a static list in the lexer + # Allows removing DC from 'Wonder Woman 49 DC Sep-Oct 1951' + # dependent on publisher being in a static list in the lexer elif item.typ == filenamelexer.ItemType.Publisher: p.filename_info["publisher"] = item.val p.used_items.append(item) @@ -656,7 +656,7 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: # TODO: What about more esoteric numbers??? -def parse_issue_number(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: +def parse_issue_number(p: Parser) -> Callable[[Parser], Callable | None] | None: item = p.input[p.pos] if "issue" in p.filename_info: @@ -689,7 +689,7 @@ def parse_issue_number(p: Parser) -> Optional[Callable[[Parser], Optional[Callab return parse -def parse_series(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: +def parse_series(p: Parser) -> Callable[[Parser], Callable | None] | None: item = p.input[p.pos] series: list[list[filenamelexer.Item]] = [[]] @@ -854,7 +854,7 @@ def resolve_year(p: Parser) -> None: p.title_parts.remove(selected_year) -def parse_finish(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: +def parse_finish(p: Parser) -> Callable[[Parser], Callable | None] | None: resolve_year(p) # If we don't have an issue try to find it in the series @@ -865,14 +865,16 @@ def parse_finish(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: if issue_num in [x[1] for x in p.year_candidates]: p.series_parts.append(issue_num) else: - # If this number was rejected because of an operator and the operator is still there add it back e.g. 'IG-88' + # If this number was rejected because of an operator and the operator is still there add it back + # e.g. 'IG-88' if ( issue_num in p.operator_rejected and p.series_parts and p.series_parts[-1].typ == filenamelexer.ItemType.Operator ): p.series_parts.append(issue_num) - # We have no reason to not use this number as the issue number. Specifically happens when parsing 'X-Men-V1-067.cbr' + # We have no reason to not use this number as the issue number. + # Specifically happens when parsing 'X-Men-V1-067.cbr' else: p.filename_info["issue"] = issue_num.val p.used_items.append(issue_num) @@ -998,7 +1000,7 @@ def get_remainder(p: Parser) -> str: return remainder.strip() -def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]: +def parse_info_specifier(p: Parser) -> Callable[[Parser], Callable | None] | None: item = p.input[p.pos] index = p.pos @@ -1033,7 +1035,8 @@ def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Call p.used_items.append(item) p.used_items.append(number) - # This is not for the issue number it is not in either the issue or the title, assume it is the volume number and count + # This is not for the issue number it is not in either the issue or the title, + # assume it is the volume number and count elif p.issue_number_at != i.pos and i not in p.series_parts and i not in p.title_parts: p.filename_info["volume"] = i.val p.filename_info["volume_count"] = str(int(t2do.convert(number.val))) @@ -1053,12 +1056,13 @@ def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Call # Gets 03 in '03 of 6' -def get_number(p: Parser, index: int) -> Optional[filenamelexer.Item]: +def get_number(p: Parser, index: int) -> filenamelexer.Item | None: # Go backward through the filename to see if we can find what this is of eg '03 (of 6)' or '008 title 03 (of 6)' rev = p.input[:index] rev.reverse() for i in rev: - # We don't care about these types, we are looking to see if there is a number that is possibly different from the issue number for this count + # We don't care about these types, we are looking to see if there is a number that is possibly different from + # the issue number for this count if i.typ in [ filenamelexer.ItemType.LeftParen, filenamelexer.ItemType.LeftBrace, diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 9dba196..9cf4310 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -5,23 +5,23 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion possible, however lossy it might be """ - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging -from typing import Any, List, Optional, TypedDict +from typing import Any, TypedDict from comicapi import utils @@ -76,59 +76,59 @@ class GenericMetadata: def __init__(self) -> None: self.is_empty: bool = True - self.tag_origin: Optional[str] = None + self.tag_origin: str | None = None - self.series: Optional[str] = None - self.issue: Optional[str] = None - self.title: Optional[str] = None - self.publisher: Optional[str] = None - self.month: Optional[int] = None - self.year: Optional[int] = None - self.day: Optional[int] = None - self.issue_count: Optional[int] = None - self.volume: Optional[int] = None - self.genre: Optional[str] = None - self.language: Optional[str] = None # 2 letter iso code - self.comments: Optional[str] = None # use same way as Summary in CIX + self.series: str | None = None + self.issue: str | None = None + self.title: str | None = None + self.publisher: str | None = None + self.month: int | None = None + self.year: int | None = None + self.day: int | None = None + self.issue_count: int | None = None + self.volume: int | None = None + self.genre: str | None = None + self.language: str | None = None # 2 letter iso code + self.comments: str | None = None # use same way as Summary in CIX - self.volume_count: Optional[int] = None - self.critical_rating: Optional[str] = None - self.country: Optional[str] = None + self.volume_count: int | None = None + self.critical_rating: str | None = None + self.country: str | None = None - self.alternate_series: Optional[str] = None - self.alternate_number: Optional[str] = None - self.alternate_count: Optional[int] = None - self.imprint: Optional[str] = None - self.notes: Optional[str] = None - self.web_link: Optional[str] = None - self.format: Optional[str] = None - self.manga: Optional[str] = None - self.black_and_white: Optional[bool] = None - self.page_count: Optional[int] = None - self.maturity_rating: Optional[str] = None - self.community_rating: Optional[str] = None + self.alternate_series: str | None = None + self.alternate_number: str | None = None + self.alternate_count: int | None = None + self.imprint: str | None = None + self.notes: str | None = None + self.web_link: str | None = None + self.format: str | None = None + self.manga: str | None = None + self.black_and_white: bool | None = None + self.page_count: int | None = None + self.maturity_rating: str | None = None + self.community_rating: str | None = None - self.story_arc: Optional[str] = None - self.series_group: Optional[str] = None - self.scan_info: Optional[str] = None + self.story_arc: str | None = None + self.series_group: str | None = None + self.scan_info: str | None = None - self.characters: Optional[str] = None - self.teams: Optional[str] = None - self.locations: Optional[str] = None + self.characters: str | None = None + self.teams: str | None = None + self.locations: str | None = None - self.credits: List[CreditMetadata] = [] - self.tags: List[str] = [] - self.pages: List[ImageMetadata] = [] + self.credits: list[CreditMetadata] = [] + self.tags: list[str] = [] + self.pages: list[ImageMetadata] = [] # Some CoMet-only items - self.price: Optional[str] = None - self.is_version_of: Optional[str] = None - self.rights: Optional[str] = None - self.identifier: Optional[str] = None - self.last_mark: Optional[str] = None - self.cover_image: Optional[str] = None + self.price: str | None = None + self.is_version_of: str | None = None + self.rights: str | None = None + self.identifier: str | None = None + self.last_mark: str | None = None + self.cover_image: str | None = None - def overlay(self, new_md: "GenericMetadata") -> None: + def overlay(self, new_md: GenericMetadata) -> None: """Overlay a metadata object on this one That is, when the new object has non-None values, over-write them @@ -198,7 +198,7 @@ class GenericMetadata: if len(new_md.pages) > 0: assign("pages", new_md.pages) - def overlay_credits(self, new_credits: List[CreditMetadata]) -> None: + def overlay_credits(self, new_credits: list[CreditMetadata]) -> None: for c in new_credits: primary = bool("primary" in c and c["primary"]) @@ -220,8 +220,7 @@ class GenericMetadata: self.pages.append(page_dict) def get_archive_page_index(self, pagenum: int) -> int: - # convert the displayed page number to the page index of the file in - # the archive + # convert the displayed page number to the page index of the file in the archive if pagenum < len(self.pages): return int(self.pages[pagenum]["Image"]) @@ -374,9 +373,9 @@ md_test.volume = 1 md_test.genre = "Sci-Fi" md_test.language = "en" md_test.comments = ( - "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." + "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." ) md_test.volume_count = None md_test.critical_rating = None diff --git a/comicapi/issuestring.py b/comicapi/issuestring.py index 2e425a6..c468fc8 100644 --- a/comicapi/issuestring.py +++ b/comicapi/issuestring.py @@ -4,31 +4,29 @@ Class for handling the odd permutations of an 'issue number' that the comics industry throws at us. e.g.: "12", "12.1", "0", "-1", "5AU", "100-2" """ - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging import unicodedata -from typing import Optional logger = logging.getLogger(__name__) class IssueString: - def __init__(self, text: Optional[str]) -> None: + def __init__(self, text: str | None) -> None: # break up the issue number string into 2 parts: the numeric and suffix string. # (assumes that the numeric portion is always first) @@ -52,8 +50,7 @@ class IssueString: # if it's still not numeric at start skip it if text[start].isdigit() or text[start] == ".": - # walk through the string, look for split point (the first - # non-numeric) + # walk through the string, look for split point (the first non-numeric) decimal_count = 0 for idx in range(start, len(text)): if text[idx] not in "0123456789.": @@ -71,8 +68,7 @@ class IssueString: if text[idx - 1] == "." and len(text) != idx: idx = idx - 1 - # if there is no numeric after the minus, make the minus part of - # the suffix + # if there is no numeric after the minus, make the minus part of the suffix if idx == 1 and start == 1: idx = 0 @@ -113,7 +109,7 @@ class IssueString: return num_s - def as_float(self) -> Optional[float]: + def as_float(self) -> float | None: # return the float, with no suffix if len(self.suffix) == 1 and self.suffix.isnumeric(): return (self.num or 0) + unicodedata.numeric(self.suffix) diff --git a/comicapi/utils.py b/comicapi/utils.py index 63ff7f9..a135d78 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -1,18 +1,18 @@ """Some generic utilities""" - # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import json import logging @@ -21,7 +21,7 @@ import pathlib import re import unicodedata from collections import defaultdict -from typing import Any, Iterable, List, Optional, Union +from typing import Any, Mapping import pycountry @@ -32,7 +32,7 @@ class UtilsVars: already_fixed_encoding = False -def get_recursive_filelist(pathlist: List[str]) -> List[str]: +def get_recursive_filelist(pathlist: list[str]) -> list[str]: """Get a recursive list of of all files under all path items in the list""" filelist = [] @@ -55,7 +55,7 @@ def get_recursive_filelist(pathlist: List[str]) -> List[str]: return filelist -def list_to_string(lst: List[Union[str, Any]]) -> str: +def list_to_string(lst: list[str | Any]) -> str: string = "" if lst is not None: for item in lst: @@ -77,7 +77,7 @@ def add_to_path(dirname: str) -> None: os.environ["PATH"] = dirname + os.pathsep + os.environ["PATH"] -def which(program: str) -> Optional[str]: +def which(program: str) -> str | None: """Returns path of the executable, if it exists""" def is_exe(fpath: str) -> bool: @@ -173,9 +173,9 @@ def unique_file(file_name: str) -> str: counter += 1 -languages: dict[Optional[str], Optional[str]] = defaultdict(lambda: None) +languages: dict[str | None, str | None] = defaultdict(lambda: None) -countries: dict[Optional[str], Optional[str]] = defaultdict(lambda: None) +countries: dict[str | None, str | None] = defaultdict(lambda: None) for c in pycountry.countries: if "alpha_2" in c._fields: @@ -186,11 +186,11 @@ for lng in pycountry.languages: languages[lng.alpha_2] = lng.name -def get_language_from_iso(iso: Optional[str]) -> Optional[str]: +def get_language_from_iso(iso: str | None) -> str | None: return languages[iso] -def get_language(string: Optional[str]) -> Optional[str]: +def get_language(string: str | None) -> str | None: if string is None: return None @@ -199,7 +199,7 @@ def get_language(string: Optional[str]) -> Optional[str]: if lang is None: try: return str(pycountry.languages.lookup(string).name) - except: + except LookupError: return None return lang @@ -217,7 +217,7 @@ def get_publisher(publisher: str) -> tuple[str, str]: return (imprint, publisher) -def update_publishers(new_publishers: dict[str, dict[str, str]]) -> None: +def update_publishers(new_publishers: Mapping[str, Mapping[str, str]]) -> None: for publisher in new_publishers: if publisher in publishers: publishers[publisher].update(new_publishers[publisher]) @@ -233,7 +233,7 @@ class ImprintDict(dict): if the key does not exist the key is returned as the publisher unchanged """ - def __init__(self, publisher: str, mapping: Iterable = (), **kwargs: Any): + def __init__(self, publisher: str, mapping=(), **kwargs) -> None: super().__init__(mapping, **kwargs) self.publisher = publisher @@ -249,7 +249,7 @@ class ImprintDict(dict): else: return (item, self.publisher, True) - def copy(self) -> "ImprintDict": + def copy(self) -> ImprintDict: return ImprintDict(self.publisher, super().copy()) diff --git a/comictagger.py b/comictagger.py index 05badf3..c12c6b1 100755 --- a/comictagger.py +++ b/comictagger.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import localefix from comictaggerlib.main import ctmain diff --git a/comictaggerlib/__init__.py b/comictaggerlib/__init__.py index e69de29..9d48db4 100644 --- a/comictaggerlib/__init__.py +++ b/comictaggerlib/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 9263707..fe23319 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -1,22 +1,23 @@ """A PyQT4 dialog to select from automated issue matches""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import os -from typing import Callable, List +from typing import Callable from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -36,7 +37,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): def __init__( self, parent: QtWidgets.QWidget, - match_set_list: List[MultipleMatch], + match_set_list: list[MultipleMatch], style: int, fetch_func: Callable[[IssueResult], GenericMetadata], settings: ComicTaggerSettings, diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py index 2f93529..f8decb3 100644 --- a/comictaggerlib/autotagprogresswindow.py +++ b/comictaggerlib/autotagprogresswindow.py @@ -1,19 +1,19 @@ """A PyQT4 dialog to show ID log and progress""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index c2f3504..492187b 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -1,19 +1,19 @@ """A PyQT4 dialog to confirm and set options for auto-tag""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index b7beacf..b1342c7 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -1,21 +1,21 @@ """A class to manage modifying metadata specifically for CBL/CBI""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging -from typing import Optional from comicapi.genericmetadata import CreditMetadata, GenericMetadata from comictaggerlib.settings import ComicTaggerSettings @@ -34,7 +34,7 @@ class CBLTransformer: if item.lower() not in (tag.lower() for tag in self.metadata.tags): self.metadata.tags.append(item) - def add_string_list_to_tags(str_list: Optional[str]) -> None: + def add_string_list_to_tags(str_list: str | None) -> None: if str_list: items = [s.strip() for s in str_list.split(",")] for item in items: @@ -43,8 +43,8 @@ class CBLTransformer: if self.settings.assume_lone_credit_is_primary: # helper - def set_lone_primary(role_list: list[str]) -> tuple[Optional[CreditMetadata], int]: - lone_credit: Optional[CreditMetadata] = None + def set_lone_primary(role_list: list[str]) -> tuple[CreditMetadata | None, int]: + lone_credit: CreditMetadata | None = None count = 0 for c in self.metadata.credits: if c["role"].lower() in role_list: diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index b335a54..83e213e 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -1,20 +1,20 @@ #!/usr/bin/python - """ComicTagger CLI functions""" - +# # Copyright 2013 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import json @@ -539,7 +539,7 @@ def process_file_cli( if opts.delete_after_zip_export: try: os.unlink(rar_file) - except: + except OSError: logger.exception(msg_hdr + "Error deleting original RAR after export") delete_success = False else: diff --git a/comictaggerlib/comicvinecacher.py b/comictaggerlib/comicvinecacher.py index 5c083d5..e671225 100644 --- a/comictaggerlib/comicvinecacher.py +++ b/comictaggerlib/comicvinecacher.py @@ -1,24 +1,25 @@ """A python class to manage caching of data from Comic Vine""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import datetime import logging import os import sqlite3 as lite -from typing import Any, Optional +from typing import Any from comicapi import utils from comictaggerlib import ctversion @@ -40,7 +41,7 @@ class ComicVineCacher: with open(self.version_file, "rb") as f: data = f.read().decode("utf-8") f.close() - except: + except Exception: pass if data != ctversion.version: self.clear_cache() @@ -51,11 +52,11 @@ class ComicVineCacher: def clear_cache(self) -> None: try: os.unlink(self.db_file) - except: + except Exception: pass try: os.unlink(self.version_file) - except: + except Exception: pass def create_cache_db(self) -> None: @@ -283,9 +284,9 @@ class ComicVineCacher: } self.upsert(cur, "issues", "id", issue["id"], data) - def get_volume_info(self, volume_id: int) -> Optional[CVVolumeResults]: + def get_volume_info(self, volume_id: int) -> CVVolumeResults | None: - result: Optional[CVVolumeResults] = None + result: CVVolumeResults | None = None con = lite.connect(self.db_file) with con: @@ -333,7 +334,10 @@ class ComicVineCacher: results: list[CVIssuesResults] = [] cur.execute( - "SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?", + ( + "SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description" + " FROM Issues WHERE volume_id = ?" + ), [volume_id], ) rows = cur.fetchall() diff --git a/comictaggerlib/comicvinetalker.py b/comictaggerlib/comicvinetalker.py index a0f30ff..6ca1219 100644 --- a/comictaggerlib/comicvinetalker.py +++ b/comictaggerlib/comicvinetalker.py @@ -1,25 +1,26 @@ """A python class to manage communication with Comic Vine's REST API""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import json import logging import re import time from datetime import datetime -from typing import Any, Callable, Optional, Union, cast +from typing import Any, Callable, cast import requests from bs4 import BeautifulSoup @@ -71,7 +72,7 @@ def list_fetch_complete(url_list: list[str]) -> None: ... -def url_fetch_complete(image_url: str, thumb_url: Optional[str]) -> None: +def url_fetch_complete(image_url: str, thumb_url: str | None) -> None: ... @@ -97,14 +98,14 @@ class ComicVineTalker: # key that is registered to comictagger default_api_key = "27431e6787042105bd3e47e169a624521f89f3a4" - self.issue_id: Optional[int] = None + self.issue_id: int | None = None if ComicVineTalker.api_key == "": self.api_key = default_api_key else: self.api_key = ComicVineTalker.api_key - self.log_func: Optional[Callable[[str], None]] = None + self.log_func: Callable[[str], None] | None = None if qt_available: self.nam = QtNetwork.QNetworkAccessManager() @@ -118,7 +119,7 @@ class ComicVineTalker: else: self.log_func(text) - def parse_date_str(self, date_str: str) -> tuple[Optional[int], Optional[int], Optional[int]]: + def parse_date_str(self, date_str: str) -> tuple[int | None, int | None, int | None]: day = None month = None year = None @@ -142,7 +143,7 @@ class ComicVineTalker: # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" return cv_response["status_code"] != 100 - except: + except Exception: return False def get_cv_content(self, url: str, params: dict[str, Any]) -> CVResult: @@ -199,7 +200,7 @@ class ComicVineTalker: raise ComicVineTalkerException(ComicVineTalkerException.Unknown, "Error on Comic Vine server") def search_for_series( - self, series_name: str, callback: Optional[Callable[[int, int], None]] = None, refresh_cache: bool = False + self, series_name: str, callback: Callable[[int, int], None] | None = None, refresh_cache: bool = False ) -> list[CVVolumeResults]: # Sanitize the series name for comicvine searching, comicvine search ignore symbols @@ -372,7 +373,7 @@ class ComicVineTalker: return volume_issues_result def fetch_issues_by_volume_issue_num_and_year( - self, volume_id_list: list[int], issue_number: str, year: Union[str, int, None] + self, volume_id_list: list[int], issue_number: str, year: str | int | None ) -> list[CVIssuesResults]: volume_filter = "" for vid in volume_id_list: @@ -383,7 +384,7 @@ class ComicVineTalker: if int_year is not None: flt += f",cover_date:{int_year}-1-1|{int_year+1}-1-1" - params: dict[str, Union[str, int]] = { + params: dict[str, str | int] = { "api_key": self.api_key, "format": "json", "field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description", @@ -473,7 +474,10 @@ class ComicVineTalker: if settings.use_series_start_as_volume: metadata.volume = int(volume_results["start_year"]) - metadata.notes = f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {issue_results['id']}]" + metadata.notes = ( + f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on" + f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {issue_results['id']}]" + ) metadata.web_link = issue_results["site_detail_url"] person_credits = issue_results["person_credits"] @@ -584,7 +588,7 @@ class ComicVineTalker: # now we have the data, make it into text fmtstr = "" for w in col_widths: - fmtstr += " {{:{}}}|".format(w + 1) + fmtstr += f" {{:{w + 1}}}|" width = sum(col_widths) + len(col_widths) * 2 table_text = "" counter = 0 @@ -597,7 +601,7 @@ class ComicVineTalker: table_strings.append(table_text) newstring = newstring.format(*table_strings) - except: + except Exception: # we caught an error rebuilding the table. # just bail and remove the formatting logger.exception("table parse error") @@ -605,16 +609,16 @@ class ComicVineTalker: return newstring - def fetch_issue_date(self, issue_id: int) -> tuple[Optional[int], Optional[int]]: + def fetch_issue_date(self, issue_id: int) -> tuple[int | None, int | None]: details = self.fetch_issue_select_details(issue_id) _, month, year = self.parse_date_str(details["cover_date"] or "") return month, year - def fetch_issue_cover_urls(self, issue_id: int) -> tuple[Optional[str], Optional[str]]: + def fetch_issue_cover_urls(self, issue_id: int) -> tuple[str | None, str | None]: details = self.fetch_issue_select_details(issue_id) return details["image_url"], details["thumb_image_url"] - def fetch_issue_page_url(self, issue_id: int) -> Optional[str]: + def fetch_issue_page_url(self, issue_id: int) -> str | None: details = self.fetch_issue_select_details(issue_id) return details["site_detail_url"] @@ -736,7 +740,7 @@ class ComicVineTalker: self.nam.finished.connect(self.async_fetch_issue_cover_url_complete) self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(issue_url))) - def async_fetch_issue_cover_url_complete(self, reply: "QtNetwork.QNetworkReply") -> None: + def async_fetch_issue_cover_url_complete(self, reply: QtNetwork.QNetworkReply) -> None: # read in the response data = reply.readAll() @@ -772,7 +776,7 @@ class ComicVineTalker: self.nam.finished.connect(self.async_fetch_alternate_cover_urls_complete) self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(str(issue_page_url)))) - def async_fetch_alternate_cover_urls_complete(self, reply: "QtNetwork.QNetworkReply") -> None: + def async_fetch_alternate_cover_urls_complete(self, reply: QtNetwork.QNetworkReply) -> None: # read in the response html = str(reply.readAll()) alt_cover_url_list = self.parse_out_alt_cover_urls(html) @@ -783,7 +787,7 @@ class ComicVineTalker: ComicVineTalker.alt_url_list_fetch_complete(alt_cover_url_list) def repair_urls( - self, issue_list: Union[list[CVIssuesResults], list[CVVolumeResults], list[CVIssueDetailResults]] + self, issue_list: list[CVIssuesResults] | list[CVVolumeResults] | list[CVIssueDetailResults] ) -> None: # make sure there are URLs for the image fields for issue in issue_list: diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index f9a8e9e..d7354d3 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -3,24 +3,24 @@ Display cover images from either a local archive, or from Comic Vine. TODO: This should be re-factored using subclasses! """ - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging -from typing import Callable, Optional, Union, cast +from typing import Callable, cast from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -74,10 +74,10 @@ class Signal(QtCore.QObject): def emit_list(self, url_list: list[str]) -> None: self.alt_url_list_fetch_complete.emit(url_list) - def emit_url(self, image_url: str, thumb_url: Optional[str]) -> None: + def emit_url(self, image_url: str, thumb_url: str | None) -> None: self.url_fetch_complete.emit(image_url, thumb_url) - def emit_image(self, image_data: Union[bytes, QtCore.QByteArray]) -> None: + def emit_image(self, image_data: bytes | QtCore.QByteArray) -> None: self.image_fetch_complete.emit(image_data) @@ -99,13 +99,13 @@ class CoverImageWidget(QtWidgets.QWidget): ) self.mode: int = mode - self.page_loader: Optional[PageLoader] = None + self.page_loader: PageLoader | None = None self.showControls = True self.current_pixmap = QtGui.QPixmap() - self.comic_archive: Optional[ComicArchive] = None - self.issue_id: Optional[int] = None + self.comic_archive: ComicArchive | None = None + self.issue_id: int | None = None self.url_list: list[str] = [] if self.page_loader is not None: self.page_loader.abandoned = True @@ -193,7 +193,7 @@ class CoverImageWidget(QtWidgets.QWidget): self.update_content() - def primary_url_fetch_complete(self, primary_url: str, thumb_url: Optional[str]) -> None: + def primary_url_fetch_complete(self, primary_url: str, thumb_url: str | None) -> None: self.url_list.append(str(primary_url)) self.imageIndex = 0 self.imageCount = len(self.url_list) diff --git a/comictaggerlib/crediteditorwindow.py b/comictaggerlib/crediteditorwindow.py index 9454552..3e6c33a 100644 --- a/comictaggerlib/crediteditorwindow.py +++ b/comictaggerlib/crediteditorwindow.py @@ -1,19 +1,19 @@ """A PyQT4 dialog to edit credits""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging from typing import Any diff --git a/comictaggerlib/exportwindow.py b/comictaggerlib/exportwindow.py index fdf4e38..595fcaa 100644 --- a/comictaggerlib/exportwindow.py +++ b/comictaggerlib/exportwindow.py @@ -1,19 +1,19 @@ """A PyQT4 dialog to confirm and set options for export to zip""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 639d2dd..a98bb93 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -1,18 +1,19 @@ """Functions for renaming files based on metadata""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import calendar import logging @@ -20,7 +21,7 @@ import os import pathlib import string import sys -from typing import Any, Optional, cast +from typing import Any, cast from pathvalidate import sanitize_filename @@ -119,7 +120,7 @@ class MetadataFormatter(string.Formatter): class FileRenamer: - def __init__(self, metadata: Optional[GenericMetadata], platform: str = "auto") -> None: + def __init__(self, metadata: GenericMetadata | None, platform: str = "auto") -> None: self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})" self.smart_cleanup = True self.issue_zero_padding = 3 diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index cf78629..d617fab 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -1,22 +1,23 @@ """A PyQt5 widget for managing list of comic archive files""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import os -from typing import Callable, List, Optional, cast +from typing import Callable, cast from PyQt5 import QtCore, QtWidgets, uic @@ -130,13 +131,13 @@ class FileSelectionList(QtWidgets.QWidget): elif self.twList.rowCount() <= 0: self.listCleared.emit() - def get_archive_by_row(self, row: int) -> Optional[ComicArchive]: + def get_archive_by_row(self, row: int) -> ComicArchive | None: if row >= 0: fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole) return fi.ca return None - def get_current_archive(self) -> Optional[ComicArchive]: + def get_current_archive(self) -> ComicArchive | None: return self.get_archive_by_row(self.twList.currentRow()) def remove_selection(self) -> None: @@ -345,8 +346,8 @@ class FileSelectionList(QtWidgets.QWidget): fi.ca.read_cix() fi.ca.has_cbi() - def get_selected_archive_list(self) -> List[ComicArchive]: - ca_list: List[ComicArchive] = [] + def get_selected_archive_list(self) -> list[ComicArchive]: + ca_list: list[ComicArchive] = [] for r in range(self.twList.rowCount()): item = self.twList.item(r, FileSelectionList.dataColNum) if item.isSelected(): @@ -366,7 +367,7 @@ class FileSelectionList(QtWidgets.QWidget): self.update_row(r) self.twList.setSortingEnabled(True) - def current_item_changed_cb(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None: + def current_item_changed_cb(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None: if curr is not None: new_idx = curr.row() old_idx = -1 diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 8e0366f..076cb2d 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -1,18 +1,19 @@ """A class to manage fetching and caching of images by URL""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import datetime import logging @@ -20,7 +21,6 @@ import os import shutil import sqlite3 as lite import tempfile -from typing import Union import requests @@ -41,7 +41,7 @@ class ImageFetcherException(Exception): pass -def fetch_complete(image_data: "Union[bytes, QtCore.QByteArray]") -> None: +def fetch_complete(image_data: bytes | QtCore.QByteArray) -> None: ... @@ -106,7 +106,7 @@ class ImageFetcher: # we'll get called back when done... return bytes() - def finish_request(self, reply: "QtNetwork.QNetworkReply") -> None: + def finish_request(self, reply: QtNetwork.QNetworkReply) -> None: # read in the image data logger.debug("request finished") image_data = reply.readAll() @@ -134,7 +134,7 @@ class ImageFetcher: cur.execute("CREATE TABLE Images(url TEXT,filename TEXT,timestamp TEXT,PRIMARY KEY (url))") - def add_image_to_cache(self, url: str, image_data: "Union[bytes, QtCore.QByteArray]") -> None: + def add_image_to_cache(self, url: str, image_data: bytes | QtCore.QByteArray) -> None: con = lite.connect(self.db_file) @@ -168,7 +168,7 @@ class ImageFetcher: with open(filename, "rb") as f: image_data = f.read() f.close() - except IOError: + except OSError: pass return image_data diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py index a79dbf7..5993462 100755 --- a/comictaggerlib/imagehasher.py +++ b/comictaggerlib/imagehasher.py @@ -1,23 +1,24 @@ """A class to manage creating image content hashes, and calculate hamming distances""" - +# # Copyright 2013 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import io import logging from functools import reduce -from typing import Optional, TypeVar +from typing import TypeVar try: from PIL import Image @@ -29,12 +30,12 @@ logger = logging.getLogger(__name__) class ImageHasher: - def __init__(self, path: Optional[str] = None, data: bytes = bytes(), width: int = 8, height: int = 8) -> None: + def __init__(self, path: str | None = None, data: bytes = bytes(), width: int = 8, height: int = 8) -> None: self.width = width self.height = height if path is None and not data: - raise IOError + raise OSError try: if path is not None: @@ -86,7 +87,6 @@ class ImageHasher: result = reduce(lambda x, (y, z): x | (z << y), enumerate(map(lambda i: 0 if i < 0 else 1, filt_data)), 0) - #print("{0:016x}".format(result)) return result """ @@ -164,7 +164,6 @@ class ImageHasher: result = reduce(set_bit, enumerate(bitlist), long(0)) - #print("{0:016x}".format(result)) return result """ diff --git a/comictaggerlib/imagepopup.py b/comictaggerlib/imagepopup.py index d49f2c2..10c2515 100644 --- a/comictaggerlib/imagepopup.py +++ b/comictaggerlib/imagepopup.py @@ -1,19 +1,19 @@ """A PyQT4 widget to display a popup image""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) class ImagePopup(QtWidgets.QDialog): def __init__(self, parent: QtWidgets.QWidget, image_pixmap: QtGui.QPixmap) -> None: - super(ImagePopup, self).__init__(parent) + super().__init__(parent) uic.loadUi(ComicTaggerSettings.get_ui_file("imagepopup.ui"), self) diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index ddfa7c7..ba75ce4 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -1,23 +1,24 @@ """A class to automatically identify a comic archive""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import io import logging import sys -from typing import Any, Callable, List, Optional +from typing import Any, Callable from typing_extensions import NotRequired, TypedDict @@ -42,11 +43,11 @@ except ImportError: class SearchKeys(TypedDict): - series: Optional[str] - issue_number: Optional[str] - month: Optional[int] - year: Optional[int] - issue_count: Optional[int] + series: str | None + issue_number: str | None + month: int | None + year: int | None + issue_count: int | None class Score(TypedDict): @@ -101,8 +102,8 @@ class IssueIdentifier: self.additional_metadata = GenericMetadata() self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output - self.callback: Optional[Callable[[int, int], None]] = None - self.cover_url_callback: Optional[Callable[[bytes], None]] = None + self.callback: Callable[[int, int], None] | None = None + self.cover_url_callback: Callable[[bytes], None] | None = None self.search_result = self.result_no_matches self.cover_page_index = 0 self.cancel = False @@ -122,7 +123,7 @@ class IssueIdentifier: def set_name_length_delta_threshold(self, delta: int) -> None: self.length_delta_thresh = delta - def set_publisher_filter(self, flt: List[str]) -> None: + def set_publisher_filter(self, flt: list[str]) -> None: self.publisher_filter = flt def set_hasher_algorithm(self, algo: int) -> None: @@ -144,7 +145,7 @@ class IssueIdentifier: im = Image.open(io.BytesIO(image_data)) w, h = im.size return float(h) / float(w) - except: + except Exception: return 1.5 def crop_cover(self, image_data: bytes) -> bytes: @@ -154,7 +155,7 @@ class IssueIdentifier: try: cropped_im = im.crop((int(w / 2), 0, w, h)) - except: + except Exception: logger.exception("cropCover() error") return bytes() @@ -348,7 +349,7 @@ class IssueIdentifier: return best_score_item - def search(self) -> List[IssueResult]: + def search(self) -> list[IssueResult]: ca = self.comic_archive self.match_list = [] self.cancel = False @@ -524,7 +525,7 @@ class IssueIdentifier: hash_list, use_remote_alternates=False, ) - except: + except Exception: self.match_list = [] return self.match_list @@ -612,7 +613,7 @@ class IssueIdentifier: hash_list, use_remote_alternates=True, ) - except: + except Exception: self.match_list = [] return self.match_list self.log_msg(f"--->{score_item['score']}") diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 902706b..89c944a 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -1,22 +1,21 @@ """A PyQT4 dialog to select specific issue from list""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging -from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -65,7 +64,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): ) self.series_id = series_id - self.issue_id: Optional[int] = None + self.issue_id: int | None = None self.settings = settings self.url_fetch_thread = None self.issue_list: list[CVIssuesResults] = [] @@ -75,7 +74,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): else: self.issue_number = issue_number - self.initial_id: Optional[int] = None + self.initial_id: int | None = None self.perform_query() self.twList.resizeColumnsToContents() @@ -163,7 +162,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): def cell_double_clicked(self, r: int, c: int) -> None: self.accept() - def current_item_changed(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None: + def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None: if curr is None: return diff --git a/comictaggerlib/logwindow.py b/comictaggerlib/logwindow.py index b4d96a6..5dda5ed 100644 --- a/comictaggerlib/logwindow.py +++ b/comictaggerlib/logwindow.py @@ -1,22 +1,21 @@ """A PyQT4 dialog to a text file or log""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging -from typing import Union from PyQt5 import QtCore, QtWidgets, uic @@ -40,7 +39,7 @@ class LogWindow(QtWidgets.QDialog): ) ) - def set_text(self, text: Union[str, bytes, None]) -> None: + def set_text(self, text: str | bytes | None) -> None: try: if text is not None: if isinstance(text, bytes): diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 5427133..f71c76b 100755 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -1,21 +1,21 @@ """A python app to (automatically) tag comic archives""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import json -import logging import logging.handlers import os import pathlib @@ -24,7 +24,6 @@ import signal import sys import traceback import types -from typing import Optional import pkg_resources @@ -68,7 +67,7 @@ try: self._exception_caught.connect(show_exception_box) def exception_hook( - self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: Optional[types.TracebackType] + self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None ) -> None: """Function handling uncaught exceptions. It is triggered each time an uncaught exception occurs. @@ -167,7 +166,7 @@ def ctmain() -> None: if opts.no_gui: try: cli.cli_mode(opts, SETTINGS) - except: + except Exception: logger.exception("CLI mode failed") else: os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" @@ -186,12 +185,12 @@ def ctmain() -> None: import ctypes myappid = "comictagger" # arbitrary string - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined] # force close of console window swp_hidewindow = 0x0080 - console_wnd = ctypes.windll.kernel32.GetConsoleWindow() + console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined] if console_wnd != 0: - ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) + ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined] if platform.system() != "Linux": img = QtGui.QPixmap(ComicTaggerSettings.get_graphic("tags.png")) diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index ef0eee9..60ccfe1 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -1,18 +1,19 @@ """A PyQT4 dialog to select from automated issue matches""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import os diff --git a/comictaggerlib/optionalmsgdialog.py b/comictaggerlib/optionalmsgdialog.py index 0aa907b..6ba86b2 100644 --- a/comictaggerlib/optionalmsgdialog.py +++ b/comictaggerlib/optionalmsgdialog.py @@ -10,23 +10,23 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question", "Are you sure you wish to do this?", ) """ - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging -from typing import Union from PyQt5 import QtCore, QtWidgets @@ -71,7 +71,7 @@ class OptionalMessageDialog(QtWidgets.QDialog): layout.addWidget(self.theCheckBox) - btnbox_style: Union[QtWidgets.QDialogButtonBox.StandardButtons, QtWidgets.QDialogButtonBox.StandardButton] + btnbox_style: QtWidgets.QDialogButtonBox.StandardButtons | QtWidgets.QDialogButtonBox.StandardButton if style == StyleQuestion: btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Yes | QtWidgets.QDialogButtonBox.StandardButton.No else: diff --git a/comictaggerlib/options.py b/comictaggerlib/options.py index 46de7bc..3727190 100644 --- a/comictaggerlib/options.py +++ b/comictaggerlib/options.py @@ -1,18 +1,19 @@ """CLI options class for ComicTagger app""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import logging @@ -319,7 +320,7 @@ def launch_script(scriptfile: str, args: list[str]) -> None: def parse_cmd_line() -> argparse.Namespace: - if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1: + if platform.system() == "Darwin" and getattr(sys, "frozen", False): # remove the PSN (process serial number) argument from OS/X input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a] else: diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py index 18cb0a2..1071206 100644 --- a/comictaggerlib/pagebrowser.py +++ b/comictaggerlib/pagebrowser.py @@ -1,22 +1,22 @@ """A PyQT4 dialog to show pages of a comic archive""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import platform -from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -48,7 +48,7 @@ class PageBrowserWindow(QtWidgets.QDialog): ) ) - self.comic_archive: Optional[ComicArchive] = None + self.comic_archive: ComicArchive | None = None self.page_count = 0 self.current_page_num = 0 self.metadata = metadata diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 1066d72..9a68a52 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -1,21 +1,21 @@ """A PyQt5 widget for editing the page list info""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging -from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -102,9 +102,9 @@ class PageListEditor(QtWidgets.QWidget): self.btnUp.clicked.connect(self.move_current_up) self.btnDown.clicked.connect(self.move_current_down) self.pre_move_row = -1 - self.first_front_page: Optional[int] = None + self.first_front_page: int | None = None - self.comic_archive: Optional[ComicArchive] = None + self.comic_archive: ComicArchive | None = None self.pages_list: list[ImageMetadata] = [] def reset_page(self) -> None: diff --git a/comictaggerlib/pageloader.py b/comictaggerlib/pageloader.py index 479fc31..8049265 100644 --- a/comictaggerlib/pageloader.py +++ b/comictaggerlib/pageloader.py @@ -1,18 +1,19 @@ """A PyQT4 class to load a page image from a ComicArchive in a background thread""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging diff --git a/comictaggerlib/progresswindow.py b/comictaggerlib/progresswindow.py index 323343d..775160a 100644 --- a/comictaggerlib/progresswindow.py +++ b/comictaggerlib/progresswindow.py @@ -1,19 +1,19 @@ """A PyQt5 dialog to show ID log and progress""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from __future__ import annotations import logging diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 6bdc704..ae27818 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -1,22 +1,23 @@ """A PyQT4 dialog to confirm rename""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import os -from typing import List, TypedDict +from typing import TypedDict from PyQt5 import QtCore, QtWidgets, uic @@ -39,7 +40,7 @@ class RenameWindow(QtWidgets.QDialog): def __init__( self, parent: QtWidgets.QWidget, - comic_archive_list: List[ComicArchive], + comic_archive_list: list[ComicArchive], data_style: int, settings: ComicTaggerSettings, ) -> None: diff --git a/comictaggerlib/resulttypes.py b/comictaggerlib/resulttypes.py index 6ea04e7..31062d4 100644 --- a/comictaggerlib/resulttypes.py +++ b/comictaggerlib/resulttypes.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List, Optional, Union - from typing_extensions import NotRequired, Required, TypedDict from comicapi.comicarchive import ComicArchive @@ -16,9 +14,9 @@ class IssueResult(TypedDict): issue_title: str issue_id: int # int? volume_id: int # int? - month: Optional[int] - year: Optional[int] - publisher: Optional[str] + month: int | None + year: int | None + publisher: str | None image_url: str thumb_url: str page_url: str @@ -27,25 +25,25 @@ class IssueResult(TypedDict): class OnlineMatchResults: def __init__(self) -> None: - self.good_matches: List[str] = [] - self.no_matches: List[str] = [] - self.multiple_matches: List[MultipleMatch] = [] - self.low_confidence_matches: List[MultipleMatch] = [] - self.write_failures: List[str] = [] - self.fetch_data_failures: List[str] = [] + self.good_matches: list[str] = [] + self.no_matches: list[str] = [] + self.multiple_matches: list[MultipleMatch] = [] + self.low_confidence_matches: list[MultipleMatch] = [] + self.write_failures: list[str] = [] + self.fetch_data_failures: list[str] = [] class MultipleMatch: - def __init__(self, ca: ComicArchive, match_list: List[IssueResult]) -> None: + def __init__(self, ca: ComicArchive, match_list: list[IssueResult]) -> None: self.ca: ComicArchive = ca self.matches: list[IssueResult] = match_list class SelectDetails(TypedDict): - image_url: Optional[str] - thumb_image_url: Optional[str] - cover_date: Optional[str] - site_detail_url: Optional[str] + image_url: str | None + thumb_image_url: str | None + cover_date: str | None + site_detail_url: str | None class CVResult(TypedDict): @@ -55,14 +53,14 @@ class CVResult(TypedDict): number_of_page_results: int number_of_total_results: int status_code: int - results: Union[ - CVIssuesResults, - CVIssueDetailResults, - CVVolumeResults, - list[CVIssuesResults], - list[CVVolumeResults], - list[CVIssueDetailResults], - ] + results: ( + CVIssuesResults + | CVIssueDetailResults + | CVVolumeResults + | list[CVIssuesResults] + | list[CVVolumeResults] + | list[CVIssueDetailResults] + ) version: str diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py index 34d1602..1692fce 100644 --- a/comictaggerlib/settings.py +++ b/comictaggerlib/settings.py @@ -1,18 +1,19 @@ """Settings class for ComicTagger app""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import configparser import logging @@ -21,7 +22,7 @@ import pathlib import platform import sys import uuid -from typing import Iterator, TextIO, Union, no_type_check +from typing import Iterator, TextIO, no_type_check from comicapi import utils @@ -29,7 +30,7 @@ logger = logging.getLogger(__name__) class ComicTaggerSettings: - folder: Union[pathlib.Path, str] = "" + folder: pathlib.Path | str = "" @staticmethod def get_settings_folder() -> pathlib.Path: @@ -42,20 +43,20 @@ class ComicTaggerSettings: @staticmethod def base_dir() -> pathlib.Path: - if getattr(sys, "frozen", None): - return pathlib.Path(sys._MEIPASS) + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return pathlib.Path(sys._MEIPASS) # type: ignore[attr-defined] return pathlib.Path(__file__).parent @staticmethod - def get_graphic(filename: Union[str, pathlib.Path]) -> str: + def get_graphic(filename: str | pathlib.Path) -> str: return str(ComicTaggerSettings.base_dir() / "graphics" / filename) @staticmethod - def get_ui_file(filename: Union[str, pathlib.Path]) -> pathlib.Path: + def get_ui_file(filename: str | pathlib.Path) -> pathlib.Path: return ComicTaggerSettings.base_dir() / "ui" / filename - def __init__(self, folder: Union[str, pathlib.Path, None]) -> None: + def __init__(self, folder: str | pathlib.Path | None) -> None: # General Settings self.rar_exe_path = "" self.allow_cbi_in_rar = True @@ -168,6 +169,10 @@ class ComicTaggerSettings: # make sure rar program is now in the path for the rar class utils.add_to_path(os.path.dirname(self.rar_exe_path)) + def reset(self): + os.unlink(self.settings_file) + self.__init__(ComicTaggerSettings.folder) + def load(self) -> None: def readline_generator(f: TextIO) -> Iterator[str]: line = f.readline() @@ -175,7 +180,7 @@ class ComicTaggerSettings: yield line line = f.readline() - with open(self.settings_file, "r", encoding="utf-8") as f: + with open(self.settings_file, encoding="utf-8") as f: self.config.read_file(readline_generator(f)) self.rar_exe_path = self.config.get("settings", "rar_exe_path") diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 60b2dd6..bb1b5e0 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -1,23 +1,23 @@ """A PyQT4 dialog to enter app settings""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import os import platform -from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -171,7 +171,7 @@ class SettingsWindow(QtWidgets.QDialog): self.leRenameTemplate.setToolTip(template_tooltip) self.settings_to_form() - self.rename_error: Optional[Exception] = None + self.rename_error: Exception | None = None self.rename_test() self.btnBrowseRar.clicked.connect(self.select_rar) @@ -374,6 +374,6 @@ class SettingsWindow(QtWidgets.QDialog): class TemplateHelpWindow(QtWidgets.QDialog): def __init__(self, parent: QtWidgets.QWidget) -> None: - super(TemplateHelpWindow, self).__init__(parent) + super().__init__(parent) uic.loadUi(ComicTaggerSettings.get_ui_file("TemplateHelp.ui"), self) diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index e0f1118..5725fdb 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1,18 +1,19 @@ """The main window of the ComicTagger app""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import json @@ -25,7 +26,7 @@ import pprint import re import sys import webbrowser -from typing import Any, Callable, List, Optional, Union, cast +from typing import Any, Callable, cast from urllib.parse import urlparse import natsort @@ -75,8 +76,8 @@ class TaggerWindow(QtWidgets.QMainWindow): self, file_list: list[str], settings: ComicTaggerSettings, - parent: Optional[QtWidgets.QWidget] = None, - opts: Optional[argparse.Namespace] = None, + parent: QtWidgets.QWidget | None = None, + opts: argparse.Namespace | None = None, ) -> None: super().__init__(parent) @@ -161,14 +162,14 @@ class TaggerWindow(QtWidgets.QMainWindow): self.statusBar() self.populate_combo_boxes() - self.page_browser: Optional[PageBrowserWindow] = None - self.comic_archive: Optional[ComicArchive] = None + self.page_browser: PageBrowserWindow | None = None + self.comic_archive: ComicArchive | None = None self.dirty_flag = False self.droppedFile = None self.page_loader = None self.droppedFiles: list[str] = [] self.metadata = GenericMetadata() - self.atprogdialog: Optional[AutoTagProgressWindow] = None + self.atprogdialog: AutoTagProgressWindow | None = None self.reset_app() # set up some basic field validators @@ -444,10 +445,13 @@ Have fun! EW = ExportWindow( self, self.settings, - f"""You have selected {rar_count} archive(s) to export to Zip format. New archives will be created in the same folder as the original. + ( + f"You have selected {rar_count} archive(s) to export to Zip format. " + """ New archives will be created in the same folder as the original. -Please choose options below, and select OK. -""", + Please choose options below, and select OK. + """ + ), ) EW.adjustSize() EW.setModal(True) @@ -537,7 +541,7 @@ Please choose options below, and select OK. license_name = "Apache License 2.0" msg_box = QtWidgets.QMessageBox() - msg_box.setWindowTitle(("About " + self.appName)) + msg_box.setWindowTitle("About " + self.appName) msg_box.setTextFormat(QtCore.Qt.TextFormat.RichText) msg_box.setIconPixmap(QtGui.QPixmap(ComicTaggerSettings.get_graphic("about.png"))) msg_box.setText( @@ -750,7 +754,7 @@ Please choose options below, and select OK. # Copy all of the metadata object into the form. # Merging of metadata should be done via the overlay function def metadata_to_form(self) -> None: - def assign_text(field: Union[QtWidgets.QLineEdit, QtWidgets.QTextEdit], value: Any) -> None: + def assign_text(field: QtWidgets.QLineEdit | QtWidgets.QTextEdit, value: Any) -> None: if value is not None: field.setText(str(value)) @@ -784,7 +788,7 @@ Please choose options below, and select OK. try: self.dsbCommunityRating.setValue(md.community_rating) - except: + except ValueError: self.dsbCommunityRating.setValue(0.0) if md.format is not None and md.format != "": @@ -1333,7 +1337,7 @@ Please choose options below, and select OK. try: result = urlparse(web_link) valid = all([result.scheme in ["http", "https"], result.netloc]) - except: + except ValueError: pass if valid: @@ -1720,7 +1724,7 @@ Please choose options below, and select OK. ii.set_cover_url_callback(self.atprogdialog.set_test_image) ii.set_name_length_delta_threshold(dlg.name_length_match_tolerance) - matches: List[IssueResult] = ii.search() + matches: list[IssueResult] = ii.search() result = ii.search_result @@ -1802,10 +1806,10 @@ Please choose options below, and select OK. atstartdlg = AutoTagStartWindow( self, self.settings, - f"""You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to. - -Please choose options below, and select OK to Auto-Tag. -""", + ( + 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." + ), ) atstartdlg.adjustSize() @@ -2059,7 +2063,7 @@ Please choose options below, and select OK to Auto-Tag. new_w = self.scrollArea.width() - scrollbar_w - 5 self.scrollAreaWidgetContents.resize(new_w, self.scrollAreaWidgetContents.height()) - def resizeEvent(self, ev: Optional[QtGui.QResizeEvent]) -> None: + def resizeEvent(self, ev: QtGui.QResizeEvent | None) -> None: self.splitter_moved_event(0, 0) def tab_changed(self, idx: int) -> None: diff --git a/comictaggerlib/ui/TemplateHelp.ui b/comictaggerlib/ui/TemplateHelp.ui index f663fbe..421e651 100644 --- a/comictaggerlib/ui/TemplateHelp.ui +++ b/comictaggerlib/ui/TemplateHelp.ui @@ -33,7 +33,7 @@ <html> - <head> + <head> <style> table { font-family: arial, sans-serif; @@ -52,12 +52,12 @@ tr:nth-child(even) { } </style> </head> - <body> - <h1 style="text-align: center">Template help</h1> - <p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the + <body> + <h1 style="text-align: center">Template help</h1> + <p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the - <a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p> - Accepts the following variables: + <a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p> + Accepts the following variables: <table> <tr> <th>Tag name</th> @@ -118,7 +118,7 @@ Spider-Geddon 1 (2018) Spider-Geddon #1 - New Players; Check In </pre> - </body> + </body> </html> Qt::TextBrowserInteraction diff --git a/comictaggerlib/ui/__init__.py b/comictaggerlib/ui/__init__.py index e69de29..9d48db4 100644 --- a/comictaggerlib/ui/__init__.py +++ b/comictaggerlib/ui/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index 3e663de..23aa94b 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -1,9 +1,10 @@ """Some utilities for the GUI""" +from __future__ import annotations + import io import logging import traceback -from typing import Optional from comictaggerlib.settings import ComicTaggerSettings @@ -79,7 +80,7 @@ if qt_available: img.load(ComicTaggerSettings.get_graphic("nocover.png")) return img - def qt_error(msg: str, e: Optional[Exception] = None) -> None: + def qt_error(msg: str, e: Exception | None = None) -> None: trace = "" if e: trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__)) diff --git a/comictaggerlib/versionchecker.py b/comictaggerlib/versionchecker.py index 3f8292e..5a0f7ce 100644 --- a/comictaggerlib/versionchecker.py +++ b/comictaggerlib/versionchecker.py @@ -1,18 +1,19 @@ """Version checker""" - +# # Copyright 2013 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging import platform diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py index eb0e482..8cc321c 100644 --- a/comictaggerlib/volumeselectionwindow.py +++ b/comictaggerlib/volumeselectionwindow.py @@ -1,21 +1,21 @@ """A PyQT4 dialog to select specific series/volume from list""" - +# # Copyright 2012-2014 Anthony Beville - +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging -from typing import Optional from PyQt5 import QtCore, QtWidgets, uic from PyQt5.QtCore import pyqtSignal @@ -44,7 +44,7 @@ class SearchThread(QtCore.QThread): QtCore.QThread.__init__(self) self.series_name = series_name self.refresh: bool = refresh - self.error_code: Optional[int] = None + self.error_code: int | None = None self.cv_error = False self.cv_search_results: list[CVVolumeResults] = [] @@ -95,7 +95,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog): parent: QtWidgets.QWidget, series_name: str, issue_number: str, - year: Optional[int], + year: int | None, issue_count: int, cover_index_list: list[int], comic_archive: ComicArchive, @@ -132,11 +132,11 @@ class VolumeSelectionWindow(QtWidgets.QDialog): self.immediate_autoselect = autoselect self.cover_index_list = cover_index_list self.cv_search_results: list[CVVolumeResults] = [] - self.ii: Optional[IssueIdentifier] = None - self.iddialog: Optional[IDProgressWindow] = None - self.id_thread: Optional[IdentifyThread] = None - self.progdialog: Optional[QtWidgets.QProgressDialog] = None - self.search_thread: Optional[SearchThread] = None + self.ii: IssueIdentifier | None = None + self.iddialog: IDProgressWindow | None = None + self.id_thread: IdentifyThread | None = None + self.progdialog: QtWidgets.QProgressDialog | None = None + self.search_thread: SearchThread | None = None self.use_filter = self.settings.always_use_publisher_filter @@ -355,8 +355,8 @@ class VolumeSelectionWindow(QtWidgets.QDialog): self.cv_search_results, ) ) - except: - logger.exception("bad data error filtering filter publishers") + except Exception: + logger.exception("bad data error filtering publishers") # pre sort the data - so that we can put exact matches first afterwards # compare as str incase extra chars ie. '1976?' @@ -369,14 +369,14 @@ class VolumeSelectionWindow(QtWidgets.QDialog): key=lambda i: (str(i["start_year"]), str(i["count_of_issues"])), reverse=True, ) - except: + except Exception: logger.exception("bad data error sorting results by start_year,count_of_issues") else: try: self.cv_search_results = sorted( self.cv_search_results, key=lambda i: str(i["count_of_issues"]), reverse=True ) - except: + except Exception: logger.exception("bad data error sorting results by count_of_issues") # move sanitized matches to the front @@ -390,7 +390,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog): filter(lambda d: utils.sanitize_title(str(d["name"])) not in sanitized, self.cv_search_results) ) self.cv_search_results = exact_matches + non_matches - except: + except Exception: logger.exception("bad data error filtering exact/near matches") self.update_buttons() @@ -454,7 +454,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog): def cell_double_clicked(self, r: int, c: int) -> None: self.show_issues() - def current_item_changed(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None: + def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None: if curr is None: return diff --git a/localefix.py b/localefix.py index 70a5e77..8e1b355 100644 --- a/localefix.py +++ b/localefix.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import locale import logging import os diff --git a/mac/Makefile b/mac/Makefile index eccd026..b870ba3 100644 --- a/mac/Makefile +++ b/mac/Makefile @@ -63,4 +63,3 @@ diskimage: # move finished product to release folder mkdir -p $(TAGGER_BASE)/release mv $(DMG_FILE) $(TAGGER_BASE)/release - diff --git a/pyproject.toml b/pyproject.toml index f8bd020..a1cee20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 120 -extend-exclude = "scripts/" +force-exclude = "scripts" [tool.isort] line_length = 120 diff --git a/requirements.txt b/requirements.txt index 6bbb8ef..c94cf9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ beautifulsoup4 >= 4.1 natsort>=8.1.0 -pillow>=4.3.0 -requests==2.* pathvalidate -pycountry +pillow>=4.3.0 py7zr +pycountry +requests==2.* text2digits typing_extensions wordninja diff --git a/requirements_dev.txt b/requirements_dev.txt index 3fdc987..9e1398a 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,9 +1,9 @@ -pyinstaller>=4.10 -setuptools>=42 -setuptools_scm[toml]>=3.4 -wheel black>=22 flake8==4.* flake8-encodings isort>=5.10 -pytest==7.* \ No newline at end of file +pyinstaller>=4.10 +pytest==7.* +setuptools>=42 +setuptools_scm[toml]>=3.4 +wheel diff --git a/setup.py b/setup.py index 08cc0cf..f4de77a 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,8 @@ # It seems that post installation tweaks are broken by wheel files. # Kept here for further research +from __future__ import annotations + import glob import os diff --git a/tests/filenames.py b/testing/filenames.py similarity index 99% rename from tests/filenames.py rename to testing/filenames.py index 61cb0fc..c06bc46 100644 --- a/tests/filenames.py +++ b/testing/filenames.py @@ -9,6 +9,8 @@ format is bool(xfail: expected failure on the old parser) ) """ +from __future__ import annotations + fnames = [ ( "batman 3 title (DC).cbz", diff --git a/tests/autoimprint_test.py b/tests/autoimprint_test.py index cfc9d89..6fae3b6 100644 --- a/tests/autoimprint_test.py +++ b/tests/autoimprint_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from comicapi import utils diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index 0a7a168..3c44fe9 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -1,12 +1,14 @@ +from __future__ import annotations + import shutil -from os.path import abspath, dirname, join +from os.path import dirname, join import pytest from comicapi.comicarchive import ComicArchive, rar_support from comicapi.genericmetadata import GenericMetadata, md_test -thisdir = dirname(abspath(__file__)) +thisdir = dirname(__file__) @pytest.mark.xfail(not rar_support, reason="rar support") diff --git a/tests/filenameparser_test.py b/tests/filenameparser_test.py index 74f3e36..06f9305 100644 --- a/tests/filenameparser_test.py +++ b/tests/filenameparser_test.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import pytest -from filenames import fnames import comicapi.filenameparser +from testing.filenames import fnames @pytest.mark.parametrize("filename, reason, expected, xfail", fnames) diff --git a/tests/issuestring_test.py b/tests/issuestring_test.py index d1c66ff..49b671e 100644 --- a/tests/issuestring_test.py +++ b/tests/issuestring_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest import comicapi.issuestring diff --git a/tests/rename_test.py b/tests/rename_test.py index 585131a..8f6ae5a 100644 --- a/tests/rename_test.py +++ b/tests/rename_test.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import pathlib import pytest -from filenames import rnames from comicapi.genericmetadata import md_test from comictaggerlib.filerenamer import FileRenamer +from testing.filenames import rnames @pytest.mark.parametrize("template, move, platform, expected", rnames)