From c09f50c5686ab9d5dc9799c08221912a7b913ec9 Mon Sep 17 00:00:00 2001 From: Jiri Date: Thu, 16 May 2024 13:25:53 +0800 Subject: [PATCH 1/3] ci: add mypy check (#161) * ci: add mypy check * fix remaining mypy issues * ignore whole methods --- .github/workflows/mypy.yaml | 33 ++++++++ pyproject.toml | 6 ++ requirements-dev.txt | 1 + tagstudio/src/cli/ts_cli.py | 1 + tagstudio/src/core/field_template.py | 2 +- tagstudio/src/core/json_typing.py | 1 + tagstudio/src/core/library.py | 84 ++++++++++--------- tagstudio/src/core/palette.py | 4 +- tagstudio/src/core/ts_core.py | 2 +- tagstudio/src/qt/helpers/function_iterator.py | 5 +- tagstudio/src/qt/modals/folders_to_tags.py | 28 +++---- tagstudio/src/qt/modals/mirror_entities.py | 2 +- tagstudio/src/qt/pagination.py | 9 +- tagstudio/src/qt/ts_qt.py | 65 ++++++++------ tagstudio/src/qt/widgets/collage_icon.py | 2 +- tagstudio/src/qt/widgets/fields.py | 14 ++-- tagstudio/src/qt/widgets/item_thumb.py | 4 +- tagstudio/src/qt/widgets/panel.py | 4 +- tagstudio/src/qt/widgets/preview_panel.py | 38 ++++----- tagstudio/src/qt/widgets/tag_box.py | 9 +- tagstudio/src/qt/widgets/thumb_button.py | 4 +- tagstudio/src/qt/widgets/thumb_renderer.py | 12 ++- tagstudio/tag_studio.py | 2 +- 23 files changed, 194 insertions(+), 138 deletions(-) create mode 100644 .github/workflows/mypy.yaml diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml new file mode 100644 index 000000000..74f03dcec --- /dev/null +++ b/.github/workflows/mypy.yaml @@ -0,0 +1,33 @@ +name: MyPy + +on: [ push, pull_request ] + + +jobs: + mypy: + name: Run MyPy + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + # pyside 6.6.3 has some issue in their .pyi files + pip install PySide6==6.6.2 + pip install -r requirements.txt + pip install mypy==1.10.0 + + - name: Run MyPy + run: | + cd tagstudio + mkdir -p .mypy_cache + mypy --install-types --non-interactive + mypy --config-file ../pyproject.toml . diff --git a/pyproject.toml b/pyproject.toml index 5435d2d87..1cd4b4c89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,8 @@ [tool.ruff] exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"] + +[tool.mypy] +strict_optional = false +disable_error_code = ["union-attr", "annotation-unchecked", "import-untyped"] +explicit_package_bases = true +warn_unused_ignores = true diff --git a/requirements-dev.txt b/requirements-dev.txt index 450ee3730..38017040a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ ruff==0.4.2 pre-commit==3.7.0 Pyinstaller==6.6.0 +mypy==1.10.0 diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index 77626372f..4200e4b08 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -1,3 +1,4 @@ +# type: ignore # Copyright (C) 2024 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio diff --git a/tagstudio/src/core/field_template.py b/tagstudio/src/core/field_template.py index bb3313b60..8c42073e0 100644 --- a/tagstudio/src/core/field_template.py +++ b/tagstudio/src/core/field_template.py @@ -19,7 +19,7 @@ def __repr__(self) -> str: def to_compressed_obj(self) -> dict: """An alternative to __dict__ that only includes fields containing non-default data.""" - obj = {} + obj: dict = {} # All Field fields (haha) are mandatory, so no value checks are done. obj["id"] = self.id obj["name"] = self.name diff --git a/tagstudio/src/core/json_typing.py b/tagstudio/src/core/json_typing.py index c624e8472..ae63a8853 100644 --- a/tagstudio/src/core/json_typing.py +++ b/tagstudio/src/core/json_typing.py @@ -8,6 +8,7 @@ class JsonLibary(TypedDict("", {"ts-version": str})): fields: list # TODO macros: "list[JsonMacro]" entries: "list[JsonEntry]" + ignored_extensions: list[str] class JsonBase(TypedDict): diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index a57fc3625..c74b8d88b 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -6,14 +6,19 @@ import datetime import glob -import json import logging import os import sys import time import traceback +import typing import xml.etree.ElementTree as ET from enum import Enum +from pathlib import Path +from typing import cast, Generator + +from typing_extensions import Self + import ujson from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag @@ -42,7 +47,7 @@ def __init__(self, id: int, filename: str, path: str, fields: list[dict]) -> Non self.id = int(id) self.filename = filename self.path = path - self.fields = fields + self.fields: list[dict] = fields self.type = None # Optional Fields ====================================================== @@ -75,6 +80,7 @@ def __repr__(self) -> str: return self.__str__() def __eq__(self, __value: object) -> bool: + __value = cast(Self, object) if os.name == "nt": return ( int(self.id) == int(__value.id) @@ -129,18 +135,16 @@ def remove_tag(self, library: "Library", tag_id: int, field_index=-1): ) t.remove(tag_id) elif field_index < 0: - t: list[int] = library.get_field_attr(f, "content") + t = library.get_field_attr(f, "content") while tag_id in t: t.remove(tag_id) def add_tag( - self, library: "Library", tag_id: int, field_id: int, field_index: int = None + self, library: "Library", tag_id: int, field_id: int, field_index: int = -1 ): - # field_index: int = -1 # if self.fields: # if field_index != -1: # logging.info(f'[LIBRARY] ADD TAG to E:{self.id}, F-DI:{field_id}, F-INDEX:{field_index}') - field_index = -1 if field_index is None else field_index for i, f in enumerate(self.fields): if library.get_field_attr(f, "id") == field_id: field_index = i @@ -183,7 +187,7 @@ def __init__( self.shorthand = shorthand self.aliases = aliases # Ensures no duplicates while retaining order. - self.subtag_ids = [] + self.subtag_ids: list[int] = [] for s in subtags_ids: if int(s) not in self.subtag_ids: self.subtag_ids.append(int(s)) @@ -275,17 +279,19 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() + @typing.no_type_check def __eq__(self, __value: object) -> bool: + __value = cast(Self, __value) if os.name == "nt": return ( - int(self.id) == int(__value.id_) + int(self.id) == int(__value.id) and self.filename.lower() == __value.filename.lower() and self.path.lower() == __value.path.lower() and self.fields == __value.fields ) else: return ( - int(self.id) == int(__value.id_) + int(self.id) == int(__value.id) and self.filename == __value.filename and self.path == __value.path and self.fields == __value.fields @@ -342,7 +348,7 @@ def __init__(self) -> None: self.files_not_in_library: list[str] = [] self.missing_files: list[str] = [] self.fixed_files: list[str] = [] # TODO: Get rid of this. - self.missing_matches = {} + self.missing_matches: dict = {} # Duplicate Files # Defined by files that are exact or similar copies to others. Generated by DupeGuru. # (Filepath, Matched Filepath, Match Percentage) @@ -393,7 +399,7 @@ def __init__(self) -> None: # Tag(id=1, name='Favorite', shorthand='', aliases=['Favorited, Favorites, Likes, Liked, Loved'], subtags_ids=[], color='yellow'), # ] - self.default_fields = [ + self.default_fields: list[dict] = [ {"id": 0, "name": "Title", "type": "text_line"}, {"id": 1, "name": "Author", "type": "text_line"}, {"id": 2, "name": "Artist", "type": "text_line"}, @@ -512,8 +518,8 @@ def open_library(self, path: str) -> int: ), "r", encoding="utf-8", - ) as f: - json_dump: JsonLibary = ujson.load(f) + ) as file: + json_dump: JsonLibary = ujson.load(file) self.library_dir = str(path) self.verify_ts_folders() major, minor, patch = json_dump["ts-version"].split(".") @@ -591,7 +597,7 @@ def open_library(self, path: str) -> int: filename = entry.get("filename", "") e_path = entry.get("path", "") - fields = [] + fields: list = [] if "fields" in entry: # Cast JSON str keys to ints for f in entry["fields"]: @@ -688,14 +694,14 @@ def open_library(self, path: str) -> int: self._next_collation_id = id + 1 title = collation.get("title", "") - e_ids_and_pages = collation.get("e_ids_and_pages", "") - sort_order = collation.get("sort_order", []) - cover_id = collation.get("cover_id", []) + e_ids_and_pages = collation.get("e_ids_and_pages", []) + sort_order = collation.get("sort_order", "") + cover_id = collation.get("cover_id", -1) c = Collation( id=id, title=title, - e_ids_and_pages=e_ids_and_pages, + e_ids_and_pages=e_ids_and_pages, # type: ignore sort_order=sort_order, cover_id=cover_id, ) @@ -861,33 +867,33 @@ def clear_internal_vars(self): self.is_legacy_library = False self.entries.clear() - self._next_entry_id: int = 0 + self._next_entry_id = 0 # self.filtered_entries.clear() self._entry_id_to_index_map.clear() self._collation_id_to_index_map.clear() self.missing_matches = {} - self.dir_file_count: int = -1 + self.dir_file_count = -1 self.files_not_in_library.clear() self.missing_files.clear() self.fixed_files.clear() - self.filename_to_entry_id_map: dict[str, int] = {} + self.filename_to_entry_id_map = {} self.ignored_extensions = self.default_ext_blacklist self.tags.clear() - self._next_tag_id: int = 1000 - self._tag_strings_to_id_map: dict[str, list[int]] = {} - self._tag_id_to_cluster_map: dict[int, list[int]] = {} - self._tag_id_to_index_map: dict[int, int] = {} + self._next_tag_id = 1000 + self._tag_strings_to_id_map = {} + self._tag_id_to_cluster_map = {} + self._tag_id_to_index_map = {} self._tag_entry_ref_map.clear() - def refresh_dir(self): + def refresh_dir(self) -> Generator: """Scans a directory for files, and adds those relative filenames to internal variables.""" # Reset file interfacing variables. # -1 means uninitialized, aka a scan like this was never attempted before. - self.dir_file_count: int = 0 + self.dir_file_count = 0 self.files_not_in_library.clear() # Scans the directory for files, keeping track of: @@ -1210,6 +1216,7 @@ def fix_missing_files(self): # (int, str) self._map_filenames_to_entry_ids() + # TODO - the type here doesnt match but I cant reproduce calling this self.remove_missing_matches(fixed_indices) # for i in fixed_indices: @@ -1330,10 +1337,11 @@ def get_collation(self, collation_id: int) -> Collation: return self.collations[self._collation_id_to_index_map[int(collation_id)]] # @deprecated('Use new Entry ID system.') - def get_entry_from_index(self, index: int) -> Entry: + def get_entry_from_index(self, index: int) -> Entry | None: """Returns a Library Entry object given its index in the unfiltered Entries list.""" if self.entries: return self.entries[int(index)] + return None # @deprecated('Use new Entry ID system.') def get_entry_id_from_filepath(self, filename): @@ -1368,7 +1376,7 @@ def search_library( if query: # start_time = time.time() - query: str = query.strip().lower() + query = query.strip().lower() query_words: list[str] = query.split(" ") all_tag_terms: list[str] = [] only_untagged: bool = "untagged" in query or "no tags" in query @@ -1548,7 +1556,7 @@ def search_library( else: for entry in self.entries: added = False - allowed_ext: bool = ( + allowed_ext = ( os.path.splitext(entry.filename)[1][1:].lower() not in self.ignored_extensions ) @@ -1756,7 +1764,7 @@ def search_tags( # if context and id_weights: # time.sleep(3) - [final.append(idw[0]) for idw in id_weights if idw[0] not in final] + [final.append(idw[0]) for idw in id_weights if idw[0] not in final] # type: ignore # print(f'Final IDs: \"{[self.get_tag_from_id(id).display_name(self) for id in final]}\"') # print('') return final @@ -1774,7 +1782,7 @@ def get_all_child_tag_ids(self, tag_id: int) -> list[int]: return subtag_ids - def filter_field_templates(self: str, query) -> list[int]: + def filter_field_templates(self, query: str) -> list[int]: """Returns a list of Field Template IDs returned from a string query.""" matches: list[int] = [] @@ -2127,12 +2135,12 @@ def add_field_to_entry(self, entry_id: int, field_id: int) -> None: def mirror_entry_fields(self, entry_ids: list[int]) -> None: """Combines and mirrors all fields across a list of given Entry IDs.""" - all_fields = [] - all_ids = [] # Parallel to all_fields + all_fields: list = [] + all_ids: list = [] # Parallel to all_fields # Extract and merge all fields from all given Entries. for id in entry_ids: if id: - entry: Entry = self.get_entry(id) + entry = self.get_entry(id) if entry and entry.fields: for field in entry.fields: # First checks if their are matching tag_boxes to append to @@ -2153,7 +2161,7 @@ def mirror_entry_fields(self, entry_ids: list[int]) -> None: # Replace each Entry's fields with the new merged ones. for id in entry_ids: - entry: Entry = self.get_entry(id) + entry = self.get_entry(id) if entry: entry.fields = all_fields @@ -2181,7 +2189,7 @@ def mirror_entry_fields(self, entry_ids: list[int]) -> None: # pass # # TODO: Implement. - def get_field_attr(self, entry_field, attribute: str): + def get_field_attr(self, entry_field: dict, attribute: str): """Returns the value of a specified attribute inside an Entry field.""" if attribute.lower() == "id": return list(entry_field.keys())[0] @@ -2236,7 +2244,7 @@ def _map_tag_strings_to_tag_id(self, tag: Tag) -> None: self._tag_strings_to_id_map[shorthand].append(tag.id) for alias in tag.aliases: - alias: str = strip_punctuation(alias).lower() + alias = strip_punctuation(alias).lower() if alias not in self._tag_strings_to_id_map: self._tag_strings_to_id_map[alias] = [] self._tag_strings_to_id_map[alias].append(tag.id) diff --git a/tagstudio/src/core/palette.py b/tagstudio/src/core/palette.py index dc67b093a..886e0bd6c 100644 --- a/tagstudio/src/core/palette.py +++ b/tagstudio/src/core/palette.py @@ -5,7 +5,7 @@ from enum import Enum -class ColorType(Enum): +class ColorType(int, Enum): PRIMARY = 0 TEXT = 1 BORDER = 2 @@ -278,7 +278,7 @@ class ColorType(Enum): } -def get_tag_color(type: ColorType, color: str): +def get_tag_color(type, color): color = color.lower() try: if type == ColorType.TEXT: diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 39d5b4a30..5e4f27250 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -300,7 +300,7 @@ def match_conditions(self, entry_id: int) -> None: # input() pass - def build_url(self, entry_id: int, source: str) -> str: + def build_url(self, entry_id: int, source: str): """Tries to rebuild a source URL given a specific filename structure.""" source = source.lower().replace("-", " ").replace("_", " ") diff --git a/tagstudio/src/qt/helpers/function_iterator.py b/tagstudio/src/qt/helpers/function_iterator.py index ff214f2fd..197f90a62 100644 --- a/tagstudio/src/qt/helpers/function_iterator.py +++ b/tagstudio/src/qt/helpers/function_iterator.py @@ -3,9 +3,8 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from types import FunctionType - from PySide6.QtCore import Signal, QObject +from typing import Callable class FunctionIterator(QObject): @@ -13,7 +12,7 @@ class FunctionIterator(QObject): value = Signal(object) - def __init__(self, function: FunctionType): + def __init__(self, function: Callable): super().__init__() self.iterable = function diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index 5163e4122..1b9333119 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -36,18 +36,18 @@ def folders_to_tags(library: Library): logging.info("Converting folders to Tags") - tree = dict(dirs={}) + tree: dict = dict(dirs={}) - def add_tag_to_tree(list: list[Tag]): + def add_tag_to_tree(items: list[Tag]): branch = tree - for tag in list: + for tag in items: if tag.name not in branch["dirs"]: branch["dirs"][tag.name] = dict(dirs={}, tag=tag) branch = branch["dirs"][tag.name] - def add_folders_to_tree(list: list[str]) -> Tag: - branch = tree - for folder in list: + def add_folders_to_tree(items: list[str]) -> Tag: + branch: dict = tree + for folder in items: if folder not in branch["dirs"]: new_tag = Tag( -1, @@ -97,18 +97,18 @@ def reverse_tag(library: Library, tag: Tag, list: list[Tag]) -> list[Tag]: def generate_preview_data(library: Library): - tree = dict(dirs={}, files=[]) + tree: dict = dict(dirs={}, files=[]) - def add_tag_to_tree(list: list[Tag]): - branch = tree - for tag in list: + def add_tag_to_tree(items: list[Tag]): + branch: dict = tree + for tag in items: if tag.name not in branch["dirs"]: branch["dirs"][tag.name] = dict(dirs={}, tag=tag, files=[]) branch = branch["dirs"][tag.name] - def add_folders_to_tree(list: list[str]) -> Tag: - branch = tree - for folder in list: + def add_folders_to_tree(items: list[str]) -> dict: + branch: dict = tree + for folder in items: if folder not in branch["dirs"]: new_tag = Tag(-1, folder, "", [], [], "green") branch["dirs"][folder] = dict(dirs={}, tag=new_tag, files=[]) @@ -220,7 +220,7 @@ def __init__(self, library: "Library", driver: "QtDriver"): self.apply_button.setMinimumWidth(100) self.apply_button.clicked.connect(self.on_apply) - self.showEvent = self.on_open + self.showEvent = self.on_open # type: ignore self.root_layout.addWidget(self.title_widget) self.root_layout.addWidget(self.desc_widget) diff --git a/tagstudio/src/qt/modals/mirror_entities.py b/tagstudio/src/qt/modals/mirror_entities.py index f168c9541..09e4bab0c 100644 --- a/tagstudio/src/qt/modals/mirror_entities.py +++ b/tagstudio/src/qt/modals/mirror_entities.py @@ -125,7 +125,7 @@ def mirror_entries(self): ) def mirror_entries_runnable(self): - mirrored = [] + mirrored: list = [] for i, dupe in enumerate(self.lib.dupe_files): # pb.setValue(i) # pb.setLabelText(f'Mirroring {i}/{len(self.lib.dupe_files)} Entries') diff --git a/tagstudio/src/qt/pagination.py b/tagstudio/src/qt/pagination.py index 64b159faf..c877922bb 100644 --- a/tagstudio/src/qt/pagination.py +++ b/tagstudio/src/qt/pagination.py @@ -292,9 +292,9 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): ).widget().setHidden(False) self.start_buffer_layout.itemAt( i - start_offset - ).widget().setText(str(i + 1)) + ).widget().setText(str(i + 1)) # type: ignore self._assign_click( - self.start_buffer_layout.itemAt(i - start_offset).widget(), + self.start_buffer_layout.itemAt(i - start_offset).widget(), # type: ignore i, ) sbc += 1 @@ -319,11 +319,12 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): self.end_buffer_layout.itemAt( i - end_offset ).widget().setHidden(False) - self.end_buffer_layout.itemAt(i - end_offset).widget().setText( + self.end_buffer_layout.itemAt(i - end_offset).widget().setText( # type: ignore str(i + 1) ) self._assign_click( - self.end_buffer_layout.itemAt(i - end_offset).widget(), i + self.end_buffer_layout.itemAt(i - end_offset).widget(), # type: ignore + i, ) else: # if self.start_buffer_layout.itemAt(i-1): diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index f10d620ae..3cbdd9dc9 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -13,6 +13,7 @@ import os import sys import time +import typing import webbrowser from datetime import datetime as dt from pathlib import Path @@ -117,7 +118,7 @@ def __init__( scrollbar_pos: int, page_index: int, page_count: int, - search_text: str = None, + search_text: str | None = None, thumb_size=None, spacing=None, ) -> None: @@ -165,11 +166,16 @@ class QtDriver(QObject): SIGTERM = Signal() - def __init__(self, core, args): + preview_panel: PreviewPanel + + def __init__(self, core: TagStudioCore, args): super().__init__() self.core: TagStudioCore = core self.lib = self.core.lib self.args = args + self.frame_dict: dict = {} + self.nav_frames: list[NavigationState] = [] + self.cur_frame_idx: int = -1 # self.main_window = None # self.main_window = Ui_MainWindow() @@ -193,10 +199,13 @@ def __init__(self, core, args): f"[QT DRIVER] Config File does not exist creating {str(path)}" ) logging.info(f"[QT DRIVER] Using Config File {str(path)}") - self.settings = QSettings(str(path), QSettings.IniFormat) + self.settings = QSettings(str(path), QSettings.Format.IniFormat) else: self.settings = QSettings( - QSettings.IniFormat, QSettings.UserScope, "TagStudio", "TagStudio" + QSettings.Format.IniFormat, + QSettings.Scope.UserScope, + "TagStudio", + "TagStudio", ) logging.info( f"[QT DRIVER] Config File not specified, defaulting to {self.settings.fileName()}" @@ -230,7 +239,7 @@ def setup_signals(self): signal(SIGTERM, self.signal_handler) signal(SIGQUIT, self.signal_handler) - def start(self): + def start(self) -> None: """Launches the main Qt window.""" loader = QUiLoader() @@ -257,7 +266,7 @@ def start(self): # self.main_window = loader.load(home_path) self.main_window = Ui_MainWindow() self.main_window.setWindowTitle(self.base_title) - self.main_window.mousePressEvent = self.mouse_navigation + self.main_window.mousePressEvent = self.mouse_navigation # type: ignore # self.main_window.setStyleSheet( # f'QScrollBar::{{background:red;}}' # ) @@ -273,13 +282,13 @@ def start(self): splash_pixmap = QPixmap(":/images/splash.png") splash_pixmap.setDevicePixelRatio(self.main_window.devicePixelRatio()) - self.splash = QSplashScreen(splash_pixmap, Qt.WindowStaysOnTopHint) + self.splash = QSplashScreen(splash_pixmap, Qt.WindowStaysOnTopHint) # type: ignore # self.splash.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.splash.show() if os.name == "nt": appid = "cyanvoxel.tagstudio.9" - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) # type: ignore if sys.platform != "darwin": icon = QIcon() @@ -392,7 +401,7 @@ def start(self): check_action = QAction("Open library on start", self) check_action.setCheckable(True) check_action.setChecked( - self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool) + self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool) # type: ignore ) check_action.triggered.connect( lambda checked: self.settings.setValue( @@ -447,15 +456,14 @@ def start(self): self.sort_fields_action.setToolTip("Alt+S") macros_menu.addAction(self.sort_fields_action) - folders_to_tags_action = QAction("Create Tags From Folders", menu_bar) show_libs_list_action = QAction("Show Recent Libraries", menu_bar) show_libs_list_action.setCheckable(True) show_libs_list_action.setChecked( - self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool) + self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool) # type: ignore ) show_libs_list_action.triggered.connect( lambda checked: ( - self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked), + self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked), # type: ignore self.toggle_libs_list(checked), ) ) @@ -557,9 +565,9 @@ def init_library_window(self): ) ) - self.nav_frames: list[NavigationState] = [] - self.cur_frame_idx: int = -1 - self.cur_query: str = "" + self.nav_frames = [] + self.cur_frame_idx = -1 + self.cur_query = "" self.filter_items() # self.update_thumbs() @@ -650,9 +658,9 @@ def close_library(self): title_text = f"{self.base_title}" self.main_window.setWindowTitle(title_text) - self.nav_frames: list[NavigationState] = [] - self.cur_frame_idx: int = -1 - self.cur_query: str = "" + self.nav_frames = [] + self.cur_frame_idx = -1 + self.cur_query = "" self.selected.clear() self.preview_panel.update_widgets() self.filter_items() @@ -1016,8 +1024,10 @@ def refresh_frame( self.update_thumbs() # logging.info(f'Refresh: {[len(x.contents) for x in self.nav_stack]}, Index {self.cur_page_idx}') + @typing.no_type_check def purge_item_from_navigation(self, type: ItemType, id: int): # logging.info(self.nav_frames) + # TODO - types here are ambiguous for i, frame in enumerate(self.nav_frames, start=0): while (type, id) in frame.contents: logging.info(f"Removing {id} from nav stack frame {i}") @@ -1061,7 +1071,7 @@ def _init_thumb_grid(self): sa.setWidgetResizable(True) sa.setWidget(self.flow_container) - def select_item(self, type: int, id: int, append: bool, bridge: bool): + def select_item(self, type: ItemType, id: int, append: bool, bridge: bool): """Selects one or more items in the Thumbnail Grid.""" if append: # self.selected.append((thumb_index, page_index)) @@ -1284,14 +1294,14 @@ def expand_collation(self, collation_entries: list[tuple[int, int]]): self.nav_forward([(ItemType.ENTRY, x[0]) for x in collation_entries]) # self.update_thumbs() - def get_frame_contents(self, index=0, query: str = None): + def get_frame_contents(self, index=0, query: str = ""): return ( [] if not self.frame_dict[query] else self.frame_dict[query][index], index, len(self.frame_dict[query]), ) - def filter_items(self, query=""): + def filter_items(self, query: str = ""): if self.lib: # logging.info('Filtering...') self.main_window.statusbar.showMessage( @@ -1303,7 +1313,7 @@ def filter_items(self, query=""): # self.filtered_items = self.lib.search_library(query) # 73601 Entries at 500 size should be 246 all_items = self.lib.search_library(query) - frames = [] + frames: list[list[tuple[ItemType, int]]] = [] frame_count = math.ceil(len(all_items) / self.max_results) for i in range(0, frame_count): frames.append( @@ -1349,7 +1359,8 @@ def remove_recent_library(self, item_key: str): self.settings.endGroup() self.settings.sync() - def update_libs_list(self, path: str | Path): + @typing.no_type_check + def update_libs_list(self, path: Path): """add library to list in SettingItems.LIBS_LIST""" ITEMS_LIMIT = 5 path = Path(path) @@ -1404,9 +1415,9 @@ def open_library(self, path): title_text = f"{self.base_title} - Library '{self.lib.library_dir}'" self.main_window.setWindowTitle(title_text) - self.nav_frames: list[NavigationState] = [] - self.cur_frame_idx: int = -1 - self.cur_query: str = "" + self.nav_frames = [] + self.cur_frame_idx = -1 + self.cur_query = "" self.selected.clear() self.preview_panel.update_widgets() self.filter_items() @@ -1444,7 +1455,7 @@ def create_collage(self) -> None: # ('Stretch to Fill','Stretches the media file to fill the entire collage square.'), # ('Keep Aspect Ratio','Keeps the original media file\'s aspect ratio, filling the rest of the square with black bars.') # ], prompt='', required=True) - keep_aspect = 0 + keep_aspect = False if mode in [1, 2, 3]: # TODO: Choose data visualization options here. diff --git a/tagstudio/src/qt/widgets/collage_icon.py b/tagstudio/src/qt/widgets/collage_icon.py index d83fc958b..6984110f3 100644 --- a/tagstudio/src/qt/widgets/collage_icon.py +++ b/tagstudio/src/qt/widgets/collage_icon.py @@ -86,7 +86,7 @@ def render( color = "#e22c3c" # Red if data_only_mode: - pic: Image = Image.new("RGB", size, color) + pic = Image.new("RGB", size, color) # collage.paste(pic, (y*thumb_size, x*thumb_size)) self.rendered.emit(pic) if not data_only_mode: diff --git a/tagstudio/src/qt/widgets/fields.py b/tagstudio/src/qt/widgets/fields.py index 882194070..c70390500 100644 --- a/tagstudio/src/qt/widgets/fields.py +++ b/tagstudio/src/qt/widgets/fields.py @@ -5,9 +5,9 @@ import math import os -from types import FunctionType +from types import FunctionType, MethodType from pathlib import Path -from typing import Optional +from typing import Optional, cast, Callable, Any from PIL import Image, ImageQt from PySide6.QtCore import Qt, QEvent @@ -48,7 +48,7 @@ def __init__(self, title: str = "Field", inline: bool = True) -> None: # self.editable:bool = editable self.copy_callback: FunctionType = None self.edit_callback: FunctionType = None - self.remove_callback: FunctionType = None + self.remove_callback: Callable = None button_size = 24 # self.setStyleSheet('border-style:solid;border-color:#1e1a33;border-radius:8px;border-width:2px;') @@ -129,7 +129,7 @@ def __init__(self, title: str = "Field", inline: bool = True) -> None: # self.set_inner_widget(mode) - def set_copy_callback(self, callback: Optional[FunctionType]): + def set_copy_callback(self, callback: Optional[MethodType]): try: self.copy_button.clicked.disconnect() except RuntimeError: @@ -138,7 +138,7 @@ def set_copy_callback(self, callback: Optional[FunctionType]): self.copy_callback = callback self.copy_button.clicked.connect(callback) - def set_edit_callback(self, callback: Optional[FunctionType]): + def set_edit_callback(self, callback: Optional[MethodType]): try: self.edit_button.clicked.disconnect() except RuntimeError: @@ -147,7 +147,7 @@ def set_edit_callback(self, callback: Optional[FunctionType]): self.edit_callback = callback self.edit_button.clicked.connect(callback) - def set_remove_callback(self, callback: Optional[FunctionType]): + def set_remove_callback(self, callback: Optional[Callable]): try: self.remove_button.clicked.disconnect() except RuntimeError: @@ -168,7 +168,7 @@ def set_inner_widget(self, widget: "FieldWidget"): def get_inner_widget(self) -> Optional["FieldWidget"]: if self.field_layout.itemAt(0): - return self.field_layout.itemAt(0).widget() + return cast(FieldWidget, self.field_layout.itemAt(0).widget()) return None def set_title(self, title: str): diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index 05567acbf..01964428a 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -181,7 +181,7 @@ def __init__( lambda ts, i, s, ext: ( self.update_thumb(ts, image=i), self.update_size(ts, size=s), - self.set_extension(ext), + self.set_extension(ext), # type: ignore ) ) self.thumb_button.setFlat(True) @@ -388,7 +388,7 @@ def update_size(self, timestamp: float, size: QSize): self.thumb_button.setMinimumSize(size) self.thumb_button.setMaximumSize(size) - def update_clickable(self, clickable: FunctionType = None): + def update_clickable(self, clickable: typing.Callable): """Updates attributes of a thumbnail element.""" # logging.info(f'[GUI] Updating Click Event for element {id(element)}: {id(clickable) if clickable else None}') try: diff --git a/tagstudio/src/qt/widgets/panel.py b/tagstudio/src/qt/widgets/panel.py index 7221e73d4..2d2538b27 100644 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -19,9 +19,9 @@ def __init__( widget: "PanelWidget", title: str, window_title: str, - done_callback: FunctionType = None, + done_callback: Callable = None, # cancel_callback:FunctionType=None, - save_callback: FunctionType = None, + save_callback: Callable = None, has_save: bool = False, ): # [Done] diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index d77427dd4..367936026 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -6,7 +6,6 @@ import os import time import typing -from types import FunctionType from datetime import datetime as dt import cv2 @@ -67,8 +66,8 @@ def __init__(self, library: Library, driver: "QtDriver"): self.isOpen: bool = False # self.filepath = None # self.item = None # DEPRECATED, USE self.selected - self.common_fields = [] - self.mixed_fields = [] + self.common_fields: list = [] + self.mixed_fields: list = [] self.selected: list[tuple[ItemType, int]] = [] # New way of tracking items self.tag_callback = None self.containers: list[QWidget] = [] @@ -174,7 +173,7 @@ def __init__(self, library: Library, driver: "QtDriver"): info_layout.addWidget(scroll_area) # keep list of rendered libraries to avoid needless re-rendering - self.render_libs = set() + self.render_libs: set = set() self.libs_layout = QVBoxLayout() self.fill_libs_widget(self.libs_layout) @@ -182,7 +181,8 @@ def __init__(self, library: Library, driver: "QtDriver"): self.libs_flow_container.setObjectName("librariesList") self.libs_flow_container.setLayout(self.libs_layout) self.libs_flow_container.setSizePolicy( - QSizePolicy.Preferred, QSizePolicy.Maximum + QSizePolicy.Preferred, # type: ignore + QSizePolicy.Maximum, # type: ignore ) # set initial visibility based on settings @@ -233,7 +233,7 @@ def fill_libs_widget(self, layout: QVBoxLayout): settings.beginGroup(SettingItems.LIBS_LIST) lib_items: dict[str, tuple[str, str]] = {} for item_tstamp in settings.allKeys(): - val = settings.value(item_tstamp) + val: str = settings.value(item_tstamp) # type: ignore cut_val = val if len(val) > 45: cut_val = f"{val[0:10]} ... {val[-10:]}" @@ -261,13 +261,13 @@ def clear_layout(layout_item: QVBoxLayout): if child.widget() is not None: child.widget().deleteLater() elif child.layout() is not None: - clear_layout(child.layout()) + clear_layout(child.layout()) # type: ignore # remove any potential previous items clear_layout(layout) label = QLabel("Recent Libraries") - label.setAlignment(Qt.AlignCenter) + label.setAlignment(Qt.AlignCenter) # type: ignore row_layout = QHBoxLayout() row_layout.addWidget(label) @@ -348,8 +348,8 @@ def update_image_size(self, size: tuple[int, int], ratio: float = None): # logging.info(f'') # self.preview_img.setMinimumSize(64,64) - adj_width = size[0] - adj_height = size[1] + adj_width: float = size[0] + adj_height: float = size[1] # Landscape if self.image_ratio > 1: # logging.info('Landscape') @@ -371,8 +371,8 @@ def update_image_size(self, size: tuple[int, int], ratio: float = None): # self.preview_img.setMinimumSize(s) # self.preview_img.setMaximumSize(s_max) - adj_size = QSize(adj_width, adj_height) - self.img_button_size = (adj_width, adj_height) + adj_size = QSize(int(adj_width), int(adj_height)) + self.img_button_size = (int(adj_width), int(adj_height)) self.preview_img.setMaximumSize(adj_size) self.preview_img.setIconSize(adj_size) # self.preview_img.setMinimumSize(adj_size) @@ -466,7 +466,7 @@ def update_widgets(self): ) self.file_label.setFilePath(filepath) window_title = filepath - ratio: float = self.devicePixelRatio() + ratio = self.devicePixelRatio() self.tr.render_big(time.time(), filepath, (512, 512), ratio) self.file_label.setText("\u200b".join(filepath)) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) @@ -575,7 +575,7 @@ def update_widgets(self): ) self.preview_img.setCursor(Qt.CursorShape.ArrowCursor) - ratio: float = self.devicePixelRatio() + ratio = self.devicePixelRatio() self.tr.render_big(time.time(), "", (512, 512), ratio, True) try: self.preview_img.clicked.disconnect() @@ -796,7 +796,6 @@ def write_container(self, index, field, mixed=False): # container.set_editable(True) container.set_inline(False) # Normalize line endings in any text content. - text: str = "" if not mixed: text = self.lib.get_field_attr(field, "content").replace("\r", "\n") else: @@ -836,7 +835,6 @@ def write_container(self, index, field, mixed=False): # container.set_editable(True) container.set_inline(False) # Normalize line endings in any text content. - text: str = "" if not mixed: text = self.lib.get_field_attr(field, "content").replace("\r", "\n") else: @@ -877,7 +875,7 @@ def write_container(self, index, field, mixed=False): self.lib.get_field_attr(field, "content") ) title = f"{self.lib.get_field_attr(field, 'name')} (Collation)" - text: str = f"{collation.title} ({len(collation.e_ids_and_pages)} Items)" + text = f"{collation.title} ({len(collation.e_ids_and_pages)} Items)" if len(self.selected) == 1: text += f" - Page {collation.e_ids_and_pages[[x[0] for x in collation.e_ids_and_pages].index(self.selected[0][1])][1]}" inner_container = TextWidget(title, text) @@ -953,7 +951,7 @@ def write_container(self, index, field, mixed=False): container.setHidden(False) self.place_add_field_button() - def remove_field(self, field: object): + def remove_field(self, field: dict): """Removes a field from all selected Entries, given a field object.""" for item_pair in self.selected: if item_pair[0] == ItemType.ENTRY: @@ -975,7 +973,7 @@ def remove_field(self, field: object): ) pass - def update_field(self, field: object, content): + def update_field(self, field: dict, content): """Removes a field from all selected Entries, given a field object.""" field = dict(field) for item_pair in self.selected: @@ -991,7 +989,7 @@ def update_field(self, field: object, content): ) pass - def remove_message_box(self, prompt: str, callback: FunctionType) -> int: + def remove_message_box(self, prompt: str, callback: typing.Callable) -> None: remove_mb = QMessageBox() remove_mb.setText(prompt) remove_mb.setWindowTitle("Remove Field") diff --git a/tagstudio/src/qt/widgets/tag_box.py b/tagstudio/src/qt/widgets/tag_box.py index daa72dd95..bcc551a8c 100644 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -78,7 +78,7 @@ def __init__( tsp.tag_chosen.connect(lambda x: self.add_tag_callback(x)) self.add_modal = PanelModal(tsp, title, "Add Tags") self.add_button.clicked.connect( - lambda: (tsp.update_tags(), self.add_modal.show()) + lambda: (tsp.update_tags(), self.add_modal.show()) # type: ignore ) self.set_tags(tags) @@ -137,7 +137,6 @@ def edit_tag(self, tag_id: int): has_save=True, ) # self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t)) - panel: BuildTagPanel = self.edit_modal.widget self.edit_modal.saved.connect(lambda: self.lib.update_tag(btp.build_tag())) # panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag)) self.edit_modal.show() @@ -149,7 +148,7 @@ def add_tag_callback(self, tag_id): f"[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}" ) logging.info(f"[TAG BOX WIDGET] SELECTED T:{self.driver.selected}") - id = list(self.field.keys())[0] + id: int = list(self.field.keys())[0] # type: ignore for x in self.driver.selected: self.driver.lib.get_entry(x[1]).add_tag( self.driver.lib, tag_id, field_id=id, field_index=-1 @@ -170,9 +169,9 @@ def add_tag_callback(self, tag_id): def edit_tag_callback(self, tag: Tag): self.lib.update_tag(tag) - def remove_tag(self, tag_id): + def remove_tag(self, tag_id: int): logging.info(f"[TAG BOX WIDGET] SELECTED T:{self.driver.selected}") - id = list(self.field.keys())[0] + id: int = list(self.field.keys())[0] # type: ignore for x in self.driver.selected: index = self.driver.lib.get_field_index_in_entry( self.driver.lib.get_entry(x[1]), id diff --git a/tagstudio/src/qt/widgets/thumb_button.py b/tagstudio/src/qt/widgets/thumb_button.py index d6fb68f22..7878259ca 100644 --- a/tagstudio/src/qt/widgets/thumb_button.py +++ b/tagstudio/src/qt/widgets/thumb_button.py @@ -5,7 +5,7 @@ from PySide6 import QtCore from PySide6.QtCore import QEvent -from PySide6.QtGui import QEnterEvent, QPainter, QColor, QPen, QPainterPath +from PySide6.QtGui import QEnterEvent, QPainter, QColor, QPen, QPainterPath, QPaintEvent from PySide6.QtWidgets import QWidget, QPushButton @@ -18,7 +18,7 @@ def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None: # self.clicked.connect(lambda checked: self.set_selected(True)) - def paintEvent(self, event: QEvent) -> None: + def paintEvent(self, event: QPaintEvent) -> None: super().paintEvent(event) if self.hovered or self.selected: painter = QPainter() diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 5e87d7a04..641c6171c 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -103,9 +103,7 @@ def render( adj_size: int = 1 image = None pixmap = None - final = None extension: str = None - broken_thumb = False # adj_font_size = math.floor(12 * pixelRatio) if ThumbRenderer.font_pixel_ratio != pixelRatio: ThumbRenderer.font_pixel_ratio = pixelRatio @@ -283,7 +281,7 @@ def render_big( filepath, base_size: tuple[int, int], pixelRatio: float, - isLoading=False, + isLoading: bool = False, ): """Renders a large, non-square entry/element thumbnail for the GUI.""" adj_size: int = 1 @@ -291,8 +289,6 @@ def render_big( pixmap: QPixmap = None final: Image.Image = None extension: str = None - broken_thumb = False - img_ratio = 1 # adj_font_size = math.floor(12 * pixelRatio) if ThumbRenderer.font_pixel_ratio != pixelRatio: ThumbRenderer.font_pixel_ratio = pixelRatio @@ -308,7 +304,7 @@ def render_big( if isLoading: adj_size = math.ceil((512 * pixelRatio)) - final: Image.Image = ThumbRenderer.thumb_loading_512.resize( + final = ThumbRenderer.thumb_loading_512.resize( (adj_size, adj_size), resample=Image.Resampling.BILINEAR ) qim = ImageQt.ImageQt(final) @@ -452,7 +448,9 @@ def render_big( # final.paste(hl_add, mask=hl_add.getchannel(3)) scalar = 4 rec: Image.Image = Image.new( - "RGB", tuple([d * scalar for d in image.size]), "black" + "RGB", + tuple([d * scalar for d in image.size]), # type: ignore + "black", ) draw = ImageDraw.Draw(rec) draw.rounded_rectangle( diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index cfec27c5f..948c6ff88 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -5,7 +5,7 @@ """TagStudio launcher.""" from src.core.ts_core import TagStudioCore -from src.cli.ts_cli import CliDriver +from src.cli.ts_cli import CliDriver # type: ignore from src.qt.ts_qt import QtDriver import argparse import traceback From 1bfc24b70f162552994b47e803f4a7f5a0609814 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Thu, 16 May 2024 13:28:29 +0800 Subject: [PATCH 2/3] fix: update recent libs when creating new one --- tagstudio/src/qt/ts_qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index f10d620ae..ce47d60f3 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1392,7 +1392,7 @@ def open_library(self, path): # self.lib.refresh_missing_files() # title_text = f'{self.base_title} - Library \'{self.lib.library_dir}\'' # self.main_window.setWindowTitle(title_text) - self.update_libs_list(path) + pass else: logging.info( @@ -1401,6 +1401,7 @@ def open_library(self, path): print(f"Library Creation Return Code: {self.lib.create_library(path)}") self.add_new_files_callback() + self.update_libs_list(path) title_text = f"{self.base_title} - Library '{self.lib.library_dir}'" self.main_window.setWindowTitle(title_text) From badcd72bea47e805be245d5375386d603097bb87 Mon Sep 17 00:00:00 2001 From: Michael Megrath Date: Thu, 16 May 2024 22:09:41 -0700 Subject: [PATCH 3/3] fix: Clear Edit Button on container update (#115) --- tagstudio/src/qt/widgets/preview_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 367936026..92a69d18c 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -948,6 +948,7 @@ def write_container(self, index, field, mixed=False): container.set_remove_callback( lambda: self.remove_message_box(prompt=prompt, callback=callback) ) + container.edit_button.setHidden(True) container.setHidden(False) self.place_add_field_button()