Apply pre-commit configuration

This commit is contained in:
Timmy Welch 2022-06-02 18:32:16 -07:00
parent c19ed49e05
commit fd4c453854
64 changed files with 630 additions and 547 deletions

View File

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

43
.pre-commit-config.yaml Normal file
View File

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

View File

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

View File

@ -1 +1,3 @@
from __future__ import annotations
__author__ = "dromanin"

View File

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

View File

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

View File

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

View File

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

View File

@ -127,4 +127,4 @@
"red circle Comics": "Dark Circle Comics",
"red circle": "Dark Circle Comics"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import localefix
from comictaggerlib.main import ctmain

View File

@ -0,0 +1 @@
from __future__ import annotations

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@
</property>
<property name="html">
<string>&lt;html&gt;
&lt;head&gt;
&lt;head&gt;
&lt;style&gt;
table {
font-family: arial, sans-serif;
@ -52,12 +52,12 @@ tr:nth-child(even) {
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1 style="text-align: center"&gt;Template help&lt;/h1&gt;
&lt;p&gt;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
&lt;body&gt;
&lt;h1 style="text-align: center"&gt;Template help&lt;/h1&gt;
&lt;p&gt;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
&lt;a href="https://docs.python.org/3/library/string.html#format-string-syntax"&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
Accepts the following variables:
&lt;a href="https://docs.python.org/3/library/string.html#format-string-syntax"&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
Accepts the following variables:
&lt;table&gt;
&lt;tr&gt;
&lt;th&gt;Tag name&lt;/th&gt;
@ -118,7 +118,7 @@ Spider-Geddon 1 (2018)
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/body&gt;
&lt;/html&gt;</string></property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>

View File

@ -0,0 +1 @@
from __future__ import annotations

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import locale
import logging
import os

View File

@ -63,4 +63,3 @@ diskimage:
# move finished product to release folder
mkdir -p $(TAGGER_BASE)/release
mv $(DMG_FILE) $(TAGGER_BASE)/release

View File

@ -1,6 +1,6 @@
[tool.black]
line-length = 120
extend-exclude = "scripts/"
force-exclude = "scripts"
[tool.isort]
line_length = 120

View File

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

View File

@ -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.*
pyinstaller>=4.10
pytest==7.*
setuptools>=42
setuptools_scm[toml]>=3.4
wheel

View File

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

View File

@ -9,6 +9,8 @@ format is
bool(xfail: expected failure on the old parser)
)
"""
from __future__ import annotations
fnames = [
(
"batman 3 title (DC).cbz",

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest
from comicapi import utils

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest
import comicapi.issuestring

View File

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