diff --git a/src/tagstudio/qt/mixed/media_player.py b/src/tagstudio/qt/mixed/media_player.py index 44997c28a..c7402ec63 100644 --- a/src/tagstudio/qt/mixed/media_player.py +++ b/src/tagstudio/qt/mixed/media_player.py @@ -5,7 +5,7 @@ import typing from pathlib import Path from time import gmtime -from typing import override +from typing import cast, override import structlog from PIL import Image, ImageDraw @@ -411,11 +411,11 @@ def play(self, filepath: Path) -> None: self.player.play() def load_toggle_play_icon(self, playing: bool) -> None: - icon = self.driver.rm.pause_icon if playing else self.driver.rm.play_icon + icon = cast(bytes, self.driver.rm.pause_icon if playing else self.driver.rm.play_icon) self.play_pause.load(icon) def load_mute_unmute_icon(self, muted: bool) -> None: - icon = self.driver.rm.volume_mute_icon if muted else self.driver.rm.volume_icon + icon = cast(bytes, self.driver.rm.volume_mute_icon if muted else self.driver.rm.volume_icon) self.mute_unmute.load(icon) def slider_value_changed(self, value: int) -> None: diff --git a/src/tagstudio/qt/mixed/migration_modal.py b/src/tagstudio/qt/mixed/migration_modal.py index 632cd8c49..56fd2bbb3 100644 --- a/src/tagstudio/qt/mixed/migration_modal.py +++ b/src/tagstudio/qt/mixed/migration_modal.py @@ -5,6 +5,7 @@ import traceback from pathlib import Path +from typing import cast import structlog from PySide6.QtCore import QObject, Qt, QThreadPool, Signal @@ -61,8 +62,8 @@ def __init__(self, path: Path): self.path: Path = path self.stack: list[PagedPanelState] = [] - self.json_lib: JsonLibrary = None - self.sql_lib: SqliteLibrary = None + self.json_lib: JsonLibrary = None # pyright: ignore[reportAttributeAccessIssue] + self.sql_lib: SqliteLibrary = None # pyright: ignore[reportAttributeAccessIssue] self.is_migration_initialized: bool = False self.discrepancies: list[str] = [] @@ -72,7 +73,7 @@ def __init__(self, path: Path): self.old_entry_count: int = 0 self.old_tag_count: int = 0 self.old_ext_count: int = 0 - self.old_ext_type: bool = None + self.old_ext_type: bool = None # pyright: ignore[reportAttributeAccessIssue] self.field_parity: bool = False self.path_parity: bool = False @@ -367,7 +368,7 @@ def migration_progress(self, skip_ui: bool = False): minimum=0, maximum=0, ) - pb.setCancelButton(None) + pb.setCancelButton(None) # pyright: ignore[reportArgumentType] self.body_wrapper_01.layout().addWidget(pb) try: @@ -390,7 +391,7 @@ def migration_progress(self, skip_ui: bool = False): pb.setMinimum(1), # type: ignore pb.setValue(1), # type: ignore # Enable the finish button - self.stack[1].buttons[4].setDisabled(False), + cast(QPushButtonWrapper, self.stack[1].buttons[4]).setDisabled(False), ) ) QThreadPool.globalInstance().start(r) @@ -474,7 +475,7 @@ def update_sql_value_ui(self, show_msg_box: bool = True): ) self.update_sql_value( self.ext_type_row, - self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST), + self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST), # pyright: ignore[reportArgumentType] self.old_ext_type, ) logger.info("Parity check complete!") @@ -636,18 +637,15 @@ def check_path_parity(self) -> bool: def check_subtag_parity(self) -> bool: """Check if all JSON parent tags match the new SQL parent tags.""" - sql_parent_tags: set[int] = None - json_parent_tags: set[int] = None - with Session(self.sql_lib.engine) as session: for tag in self.sql_lib.tags: tag_id = tag.id # Tag IDs start at 0 - sql_parent_tags = set( + sql_parent_tags: set[int] = set( session.scalars(select(TagParent.parent_id).where(TagParent.child_id == tag.id)) ) # JSON tags allowed self-parenting; SQL tags no longer allow this. - json_parent_tags = set(self.json_lib.get_tag(tag_id).subtag_ids) + json_parent_tags: set[int] = set(self.json_lib.get_tag(tag_id).subtag_ids) json_parent_tags.discard(tag_id) logger.info( @@ -677,16 +675,15 @@ def check_ext_type(self) -> bool: def check_alias_parity(self) -> bool: """Check if all JSON aliases match the new SQL aliases.""" - sql_aliases: set[str] = None - json_aliases: set[str] = None - with Session(self.sql_lib.engine) as session: for tag in self.sql_lib.tags: tag_id = tag.id # Tag IDs start at 0 - sql_aliases = set( + sql_aliases: set[str] = set( session.scalars(select(TagAlias.name).where(TagAlias.tag_id == tag.id)) ) - json_aliases = set([x for x in self.json_lib.get_tag(tag_id).aliases if x]) + json_aliases: set[str] = set( + [x for x in self.json_lib.get_tag(tag_id).aliases if x] + ) logger.info( "[Alias Parity]", diff --git a/src/tagstudio/qt/mixed/pagination.py b/src/tagstudio/qt/mixed/pagination.py index 7a18e56c8..e39fc048b 100644 --- a/src/tagstudio/qt/mixed/pagination.py +++ b/src/tagstudio/qt/mixed/pagination.py @@ -5,10 +5,10 @@ """A pagination widget created for TagStudio.""" -from typing import override +from typing import cast, override from PIL import Image, ImageQt -from PySide6.QtCore import QObject, QSize, Signal +from PySide6.QtCore import QSize, Signal from PySide6.QtGui import QIntValidator, QPixmap from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QWidget @@ -17,7 +17,7 @@ from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper -class Pagination(QWidget, QObject): +class Pagination(QWidget): """Widget containing controls for navigating between pages of items.""" index = Signal(int) @@ -160,8 +160,8 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): srt_scale = max(1, (7 - (end_page - index))) if page_count >= 8: - end_size = self.button_size.width() * end_scale + (3 * (end_scale - 1)) - srt_size = self.button_size.width() * srt_scale + (3 * (srt_scale - 1)) + end_size = self.button_size.width() * end_scale + (3 * (end_scale - 1)) # pyright: ignore[reportPossiblyUnboundVariable] + srt_size = self.button_size.width() * srt_scale + (3 * (srt_scale - 1)) # pyright: ignore[reportPossiblyUnboundVariable] self.end_ellipses.setMinimumWidth(end_size) self.end_ellipses.setMaximumWidth(end_size) self.start_ellipses.setMinimumWidth(srt_size) @@ -223,7 +223,10 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): str(i + 1) ) self._assign_click( - self.start_buffer_layout.itemAt(i - start_offset).widget(), + cast( + QPushButtonWrapper, + self.start_buffer_layout.itemAt(i - start_offset).widget(), + ), i, ) sbc += 1 @@ -239,7 +242,10 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): str(i + 1) ) self._assign_click( - self.end_buffer_layout.itemAt(i - end_offset).widget(), + cast( + QPushButtonWrapper, + self.end_buffer_layout.itemAt(i - end_offset).widget(), + ), i, ) else: diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index 6edf83708..2c76908d8 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -21,12 +21,14 @@ import numpy as np import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport] import py7zr +import py7zr.io import rarfile import rawpy import srctools import structlog from cv2.typing import MatLike -from mutagen import MutagenError, flac, id3, mp4 +from mutagen import flac, id3, mp4 +from mutagen._util import MutagenError from PIL import ( Image, ImageChops, @@ -119,7 +121,7 @@ def namelist(self) -> list[str]: return self.getnames() def read(self, name: str) -> bytes: - return self.extractfile(name).read() + return unwrap(self.extractfile(name)).read() type _Archive_T = ( @@ -435,9 +437,9 @@ def _render_center_icon( ) # Get icon by name - icon: Image.Image | None = self.rm.get(name) + icon: Image.Image | None = self.rm.get(name) # pyright: ignore[reportAssignmentType] if not icon: - icon = self.rm.get("file_generic") + icon = self.rm.get("file_generic") # pyright: ignore[reportAssignmentType] if not icon: icon = Image.new(mode="RGBA", size=(32, 32), color="magenta") @@ -533,9 +535,9 @@ def _render_corner_icon( ) # Get icon by name - icon: Image.Image | None = self.rm.get(name) + icon: Image.Image | None = self.rm.get(name) # pyright: ignore[reportAssignmentType] if not icon: - icon = self.rm.get("file_generic") + icon = self.rm.get("file_generic") # pyright: ignore[reportAssignmentType] if not icon: icon = Image.new(mode="RGBA", size=(32, 32), color="magenta") @@ -664,14 +666,14 @@ def _audio_album_thumb(filepath: Path, ext: str) -> Image.Image | None: artwork = Image.open(BytesIO(flac_covers[0].data)) elif ext in [".mp4", ".m4a", ".aac"]: mp4_tags: mp4.MP4 = mp4.MP4(filepath) - mp4_covers: list = unwrap(mp4_tags.get("covr")) + mp4_covers: list | None = mp4_tags.get("covr") # pyright: ignore[reportAssignmentType] if mp4_covers: artwork = Image.open(BytesIO(mp4_covers[0])) if artwork: image = artwork except ( FileNotFoundError, - id3.ID3NoHeaderError, + id3.ID3NoHeaderError, # pyright: ignore[reportPrivateImportUsage] mp4.MP4MetadataError, mp4.MP4StreamInfoError, MutagenError, @@ -768,7 +770,7 @@ def _audio_waveform_thumb( return im @staticmethod - def _blender(filepath: Path) -> Image.Image: + def _blender(filepath: Path) -> Image.Image | None: """Get an emended thumbnail from a Blender file, if a thumbnail is present. Args: @@ -956,21 +958,21 @@ def __cover_from_comic_info( cover = comic_info.find(f"./*Page[@Type='{cover_type}']") if cover is not None: pages = [f for f in archive.namelist() if f != "ComicInfo.xml"] - page_name = pages[int(cover.get("Image"))] + page_name = pages[int(unwrap(cover.get("Image")))] if page_name.endswith((".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg")): image_data = archive.read(page_name) im = Image.open(BytesIO(image_data)) return im - def _font_short_thumb(self, filepath: Path, size: int) -> Image.Image: + def _font_short_thumb(self, filepath: Path, size: int) -> Image.Image | None: """Render a small font preview ("Aa") thumbnail from a font file. Args: filepath (Path): The path of the file. size (tuple[int,int]): The size of the thumbnail. """ - im: Image.Image = None + im: Image.Image | None = None try: bg = Image.new("RGB", (size, size), color="#000000") raw = Image.new("RGB", (size * 3, size * 3), color="#000000") @@ -1024,7 +1026,7 @@ def _font_short_thumb(self, filepath: Path, size: int) -> Image.Image: return im @staticmethod - def _font_long_thumb(filepath: Path, size: int) -> Image.Image: + def _font_long_thumb(filepath: Path, size: int) -> Image.Image | None: """Render a large font preview ("Alphabet") thumbnail from a font file. Args: @@ -1033,7 +1035,7 @@ def _font_long_thumb(filepath: Path, size: int) -> Image.Image: """ # Scale the sample font sizes to the preview image # resolution,assuming the sizes are tuned for 256px. - im: Image.Image = None + im: Image.Image | None = None try: scaled_sizes: list[int] = [math.floor(x * (size / 256)) for x in FONT_SAMPLE_SIZES] bg = Image.new("RGBA", (size, size), color="#00000000") @@ -1044,7 +1046,10 @@ def _font_long_thumb(filepath: Path, size: int) -> Image.Image: for font_size in scaled_sizes: font = ImageFont.truetype(filepath, size=font_size) text_wrapped: str = wrap_full_text( - FONT_SAMPLE_TEXT, font=font, width=size, draw=draw + FONT_SAMPLE_TEXT, + font=font, # pyright: ignore[reportArgumentType] + width=size, + draw=draw, ) draw.multiline_text((0, y_offset), text_wrapped, font=font) y_offset += (len(text_wrapped.split("\n")) + lines_of_padding) * draw.textbbox( @@ -1056,13 +1061,13 @@ def _font_long_thumb(filepath: Path, size: int) -> Image.Image: return im @staticmethod - def _image_raw_thumb(filepath: Path) -> Image.Image: + def _image_raw_thumb(filepath: Path) -> Image.Image | None: """Render a thumbnail for a RAW image type. Args: filepath (Path): The path of the file. """ - im: Image.Image = None + im: Image.Image | None = None try: with rawpy.imread(str(filepath)) as raw: rgb = raw.postprocess(use_camera_wb=True) @@ -1074,8 +1079,8 @@ def _image_raw_thumb(filepath: Path) -> Image.Image: ) except ( DecompressionBombError, - rawpy._rawpy.LibRawIOError, - rawpy._rawpy.LibRawFileUnsupportedError, + rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue] + rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue] ) as e: logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) return im @@ -1111,13 +1116,13 @@ def _image_exr_thumb(filepath: Path) -> Image.Image | None: return im @staticmethod - def _image_thumb(filepath: Path) -> Image.Image: + def _image_thumb(filepath: Path) -> Image.Image | None: """Render a thumbnail for a standard image type. Args: filepath (Path): The path of the file. """ - im: Image.Image = None + im: Image.Image | None = None try: im = Image.open(filepath) if im.mode != "RGB" and im.mode != "RGBA": @@ -1144,7 +1149,7 @@ def _image_vector_thumb(filepath: Path, size: int) -> Image.Image: filepath (Path): The path of the file. size (tuple[int,int]): The size of the thumbnail. """ - im: Image.Image = None + im: Image.Image | None = None # Create an image to draw the svg to and a painter to do the drawing q_image: QImage = QImage(size, size, QImage.Format.Format_ARGB32) q_image.fill("#1e1e1e") @@ -1174,7 +1179,7 @@ def _image_vector_thumb(filepath: Path, size: int) -> Image.Image: return im @staticmethod - def _iwork_thumb(filepath: Path) -> Image.Image: + def _iwork_thumb(filepath: Path) -> Image.Image | None: """Extract and render a thumbnail for an Apple iWork (Pages, Numbers, Keynote) file. Args: @@ -1212,7 +1217,7 @@ def get_image(path: str) -> Image.Image | None: return im @staticmethod - def _model_stl_thumb(filepath: Path, size: int) -> Image.Image: + def _model_stl_thumb(filepath: Path, size: int) -> Image.Image | None: """Render a thumbnail for an STL file. Args: @@ -1223,7 +1228,7 @@ def _model_stl_thumb(filepath: Path, size: int) -> Image.Image: # The following commented code describes a method for rendering via # matplotlib. # This implementation did not play nice with multithreading. - im: Image.Image = None + im: Image.Image | None = None # # Create a new plot # matplotlib.use('agg') # figure = plt.figure() @@ -1245,13 +1250,13 @@ def _model_stl_thumb(filepath: Path, size: int) -> Image.Image: return im @staticmethod - def _pdf_thumb(filepath: Path, size: int) -> Image.Image: + def _pdf_thumb(filepath: Path, size: int) -> Image.Image | None: """Render a thumbnail for a PDF file. filepath (Path): The path of the file. size (int): The size of the icon. """ - im: Image.Image = None + im: Image.Image | None = None file: QFile = QFile(filepath) success: bool = file.open(