ci: add mypy check (#161)

* ci: add mypy check

* fix remaining mypy issues

* ignore whole methods
This commit is contained in:
Jiri
2024-05-16 13:25:53 +08:00
committed by GitHub
parent 66aecf2030
commit c09f50c568
23 changed files with 194 additions and 138 deletions

33
.github/workflows/mypy.yaml vendored Normal file
View File

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

View File

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

View File

@@ -1,3 +1,4 @@
ruff==0.4.2
pre-commit==3.7.0
Pyinstaller==6.6.0
mypy==1.10.0

View File

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

View File

@@ -19,7 +19,7 @@ class FieldTemplate:
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

View File

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

View File

@@ -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 @@ class Entry:
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 @@ class Entry:
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 @@ class Entry:
)
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 @@ class Tag:
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 @@ class Collation:
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 @@ class Library:
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 @@ class Library:
# 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 @@ class Library:
),
"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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
# (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 @@ class Library:
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 @@ class 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 @@ class 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 @@ class Library:
# 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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
# 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 @@ class Library:
# 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 @@ class Library:
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)

View File

@@ -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 @@ _TAG_COLORS = {
}
def get_tag_color(type: ColorType, color: str):
def get_tag_color(type, color):
color = color.lower()
try:
if type == ColorType.TEXT:

View File

@@ -300,7 +300,7 @@ class TagStudioCore:
# 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("_", " ")

View File

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

View File

@@ -36,18 +36,18 @@ logging.basicConfig(format="%(message)s", level=logging.INFO)
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 @@ class FoldersToTagsModal(QWidget):
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)

View File

@@ -125,7 +125,7 @@ class MirrorEntriesModal(QWidget):
)
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')

View File

@@ -292,9 +292,9 @@ class Pagination(QWidget, QObject):
).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 @@ class Pagination(QWidget, QObject):
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):

View File

@@ -13,6 +13,7 @@ import math
import os
import sys
import time
import typing
import webbrowser
from datetime import datetime as dt
from pathlib import Path
@@ -117,7 +118,7 @@ class NavigationState:
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
# 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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
)
)
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
# 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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
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 @@ class QtDriver(QObject):
# ('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.

View File

@@ -86,7 +86,7 @@ class CollageIconRenderer(QObject):
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:

View File

@@ -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 @@ class FieldContainer(QWidget):
# 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 @@ class FieldContainer(QWidget):
# 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 @@ class FieldContainer(QWidget):
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 @@ class FieldContainer(QWidget):
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 @@ class FieldContainer(QWidget):
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):

View File

@@ -181,7 +181,7 @@ class ItemThumb(FlowWidget):
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 @@ class ItemThumb(FlowWidget):
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:

View File

@@ -19,9 +19,9 @@ class PanelModal(QWidget):
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]

View File

@@ -6,7 +6,6 @@ import logging
import os
import time
import typing
from types import FunctionType
from datetime import datetime as dt
import cv2
@@ -67,8 +66,8 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
# 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 @@ class PreviewPanel(QWidget):
# 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 @@ class PreviewPanel(QWidget):
)
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 @@ class PreviewPanel(QWidget):
)
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 @@ class PreviewPanel(QWidget):
# 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 @@ class PreviewPanel(QWidget):
# 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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
)
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 @@ class PreviewPanel(QWidget):
)
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")

View File

@@ -78,7 +78,7 @@ class TagBoxWidget(FieldWidget):
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 @@ class TagBoxWidget(FieldWidget):
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 @@ class TagBoxWidget(FieldWidget):
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 @@ class TagBoxWidget(FieldWidget):
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

View File

@@ -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 @@ class ThumbButton(QPushButton):
# 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()

View File

@@ -103,9 +103,7 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
# 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(

View File

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