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)