From ea76910451a135c8778d6901b4e3cf8b053aa17f Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Mon, 17 Aug 2026 15:26:38 -0700
Subject: [PATCH 1/7] build: bump python to 3.14
---
pyproject.toml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index f5a29a706..1035bda84 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ description = "A User-Focused Photo & File Management System."
version = "9.6.4"
license = "GPL-3.0-only"
readme = "README.md"
-requires-python = ">=3.12,<3.14"
+requires-python = ">=3.14,<=3.15"
dependencies = [
"audioop-lts; python_version >= '3.13'",
"chardet~=5.2",
@@ -22,7 +22,7 @@ dependencies = [
"numpy~=2.2",
"opencv_python~=4.11",
"Pillow>=10.2,<12",
- "pillow-heif~=0.22",
+ "pillow-heif~=1.5.0",
"pillow-jxl-plugin~=1.3",
"py7zr~=1.1.3",
"pydantic~=2.10",
From bc96ba9e52472efe94beb259e5753bbf93cd9f2f Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Mon, 17 Aug 2026 15:31:53 -0700
Subject: [PATCH 2/7] ci: bump python to 3.14, update docs
---
.github/actions/setup-python/action.yml | 2 +-
docs/developing.md | 6 +++---
docs/install.md | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/actions/setup-python/action.yml b/.github/actions/setup-python/action.yml
index 821719872..8e25e2b0a 100644
--- a/.github/actions/setup-python/action.yml
+++ b/.github/actions/setup-python/action.yml
@@ -18,7 +18,7 @@ runs:
if: inputs.skip-setup != 'true'
uses: actions/setup-python@v6
with:
- python-version: '3.12'
+ python-version: '3.14'
- name: Setup uv install
if: inputs.skip-setup != 'true'
diff --git a/docs/developing.md b/docs/developing.md
index 9dba3bdec..ea01e0a09 100644
--- a/docs/developing.md
+++ b/docs/developing.md
@@ -16,7 +16,7 @@ If you wish to develop for TagStudio, you'll need to create a development enviro
## Installing Python
-Python [3.12](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
+Python [3.14](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.14.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
!!! info "Python Aliases"
@@ -35,8 +35,8 @@ python --version
If you choose to install Python using pyenv, please refer to the following instructions:
1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system.
-2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation).
-3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
+2. Install the appropriate Python version with pyenv by running `pyenv install 3.14` (This will **not** mess with your existing Python installation).
+3. Navigate to the repository root folder in your terminal and run `pyenv local 3.14`. You could alternatively use `pyenv shell 3.14` or `pyenv global 3.14` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
---
diff --git a/docs/install.md b/docs/install.md
index 3abb853d5..144ed8d42 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -36,7 +36,7 @@ TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-
### :fontawesome-brands-python: Installing with PIP
-TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.12 and PIP installed if you choose to install using this method.
+TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.14 and PIP installed if you choose to install using this method.
The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).
From 6452664b2ad7febf52b4680bb530ee745bc88879 Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Tue, 18 Aug 2026 13:45:11 -0700
Subject: [PATCH 3/7] build: remove version condition for audioop-lts
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 1035bda84..db2a3ac9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,7 @@ license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.14,<=3.15"
dependencies = [
- "audioop-lts; python_version >= '3.13'",
+ "audioop-lts~=0.2.2",
"chardet~=5.2",
"ffmpeg-python~=0.2",
"humanfriendly==10.*",
From bcca08556e0e4657828c4b690d0fa657e489a524 Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Tue, 18 Aug 2026 14:05:35 -0700
Subject: [PATCH 4/7] chore: formatting pass
---
src/tagstudio/core/library/alchemy/enums.py | 28 +++++++++----------
src/tagstudio/core/library/alchemy/models.py | 18 ++++++------
src/tagstudio/core/library/json/library.py | 2 +-
src/tagstudio/core/query_lang/ast.py | 8 +++---
src/tagstudio/core/query_lang/tokenizer.py | 4 +--
src/tagstudio/core/utils/str_formatting.py | 2 +-
src/tagstudio/i18n/translations.py | 2 +-
src/tagstudio/previews/renderers/archive.py | 2 +-
src/tagstudio/qt/app_settings.py | 2 +-
.../qt/controllers/fix_ignored_modal.py | 2 +-
src/tagstudio/qt/controllers/inspector.py | 2 +-
.../qt/controllers/library_info_window.py | 2 +-
src/tagstudio/qt/controllers/main_window.py | 10 +++----
.../merge_dupe_entries_progress.py | 2 +-
src/tagstudio/qt/controllers/preview_thumb.py | 2 +-
src/tagstudio/qt/controllers/search_panel.py | 2 +-
src/tagstudio/qt/controllers/tag_box.py | 2 +-
src/tagstudio/qt/mixed/color_box.py | 4 +--
src/tagstudio/qt/mixed/datetime_picker.py | 2 +-
src/tagstudio/qt/mixed/drop_import_modal.py | 2 +-
src/tagstudio/qt/mixed/field_containers.py | 4 +--
src/tagstudio/qt/mixed/field_widget.py | 2 +-
src/tagstudio/qt/mixed/file_attributes.py | 2 +-
src/tagstudio/qt/mixed/fix_dupe_files.py | 2 +-
src/tagstudio/qt/mixed/fix_unlinked.py | 2 +-
src/tagstudio/qt/mixed/folders_to_tags.py | 6 ++--
src/tagstudio/qt/mixed/item_thumb.py | 4 +--
src/tagstudio/qt/mixed/landing.py | 2 +-
src/tagstudio/qt/mixed/media_player.py | 4 +--
src/tagstudio/qt/mixed/migration_modal.py | 2 +-
.../qt/mixed/mirror_entries_modal.py | 2 +-
.../qt/mixed/remove_ignored_modal.py | 2 +-
.../qt/mixed/remove_unlinked_modal.py | 2 +-
src/tagstudio/qt/mixed/settings_panel.py | 8 +++---
src/tagstudio/qt/mixed/tag_color_label.py | 2 +-
src/tagstudio/qt/mixed/tag_color_manager.py | 2 +-
src/tagstudio/qt/mixed/tag_color_preview.py | 2 +-
src/tagstudio/qt/mixed/tag_widget.py | 2 +-
src/tagstudio/qt/resource_manager.py | 2 +-
.../qt/views/fix_ignored_modal_view.py | 2 +-
src/tagstudio/qt/views/inspector_view.py | 2 +-
.../qt/views/layouts/thumb_grid_layout.py | 2 +-
.../qt/views/library_info_window_view.py | 2 +-
src/tagstudio/qt/views/preview_thumb_view.py | 2 +-
src/tagstudio/qt/views/tag_box_view.py | 2 +-
45 files changed, 82 insertions(+), 84 deletions(-)
diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py
index 7e16199c9..0c3c55c43 100644
--- a/src/tagstudio/core/library/alchemy/enums.py
+++ b/src/tagstudio/core/library/alchemy/enums.py
@@ -57,7 +57,7 @@ class TagColorEnum(enum.IntEnum):
OLIVE = 37
@staticmethod
- def get_color_from_str(color_name: str) -> "TagColorEnum":
+ def get_color_from_str(color_name: str) -> TagColorEnum:
for color in TagColorEnum:
if color.name == color_name.upper().replace(" ", "_"):
return color
@@ -99,17 +99,15 @@ def ast(self) -> AST | None:
return Parser(self.query).parse()
@classmethod
- def show_all(cls) -> "BrowsingState":
+ def show_all(cls) -> BrowsingState:
return BrowsingState()
@classmethod
- def from_search_query(cls, search_query: str) -> "BrowsingState":
+ def from_search_query(cls, search_query: str) -> BrowsingState:
return cls(query=search_query)
@classmethod
- def from_tag_id(
- cls, tag_id: int | str, state: "BrowsingState | None" = None
- ) -> "BrowsingState":
+ def from_tag_id(cls, tag_id: int | str, state: BrowsingState | None = None) -> BrowsingState:
"""Create and return a BrowsingState object given a tag ID.
Args:
@@ -124,35 +122,35 @@ def from_tag_id(
return cls(query=f"tag_id:{str(tag_id)}")
@classmethod
- def from_path(cls, path: Path | str) -> "BrowsingState":
+ def from_path(cls, path: Path | str) -> BrowsingState:
return cls(query=f'path:"{str(path).strip()}"')
@classmethod
- def from_mediatype(cls, mediatype: str) -> "BrowsingState":
+ def from_mediatype(cls, mediatype: str) -> BrowsingState:
return cls(query=f"mediatype:{mediatype}")
@classmethod
- def from_filetype(cls, filetype: str) -> "BrowsingState":
+ def from_filetype(cls, filetype: str) -> BrowsingState:
return cls(query=f"filetype:{filetype}")
@classmethod
- def from_tag_name(cls, tag_name: str) -> "BrowsingState":
+ def from_tag_name(cls, tag_name: str) -> BrowsingState:
return cls(query=f'tag:"{tag_name}"')
- def with_page_index(self, index: int) -> "BrowsingState":
+ def with_page_index(self, index: int) -> BrowsingState:
return replace(self, page_index=index)
- def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
+ def with_sorting_mode(self, mode: SortingModeEnum) -> BrowsingState:
seed = self.random_seed
if mode == SortingModeEnum.RANDOM:
seed = random.random()
return replace(self, sorting_mode=mode, random_seed=seed)
- def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
+ def with_sorting_direction(self, ascending: bool) -> BrowsingState:
return replace(self, ascending=ascending)
- def with_search_query(self, search_query: str) -> "BrowsingState":
+ def with_search_query(self, search_query: str) -> BrowsingState:
return replace(self, query=search_query)
- def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
+ def with_show_hidden_entries(self, show_hidden_entries: bool) -> BrowsingState:
return replace(self, show_hidden_entries=show_hidden_entries)
diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py
index c7bd0731b..4daf42d58 100644
--- a/src/tagstudio/core/library/alchemy/models.py
+++ b/src/tagstudio/core/library/alchemy/models.py
@@ -41,7 +41,7 @@ class TagAlias(Base):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"))
- tag: Mapped["Tag"] = relationship(back_populates="aliases")
+ tag: Mapped[Tag] = relationship(back_populates="aliases")
def __init__(self, name: str, tag_id: int | None = None):
self.name = name
@@ -97,14 +97,14 @@ class Tag(Base):
is_hidden: Mapped[bool]
icon: Mapped[str | None]
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
- parent_tags: Mapped[set["Tag"]] = relationship(
+ parent_tags: Mapped[set[Tag]] = relationship(
secondary=TagParent.__tablename__,
primaryjoin="Tag.id == TagParent.child_id",
secondaryjoin="Tag.id == TagParent.parent_id",
back_populates="parent_tags",
)
disambiguation_id: Mapped[int | None]
- category_exclusions: Mapped[set["Tag"]] = relationship(
+ category_exclusions: Mapped[set[Tag]] = relationship(
secondary=CategoryExclusion.__tablename__,
primaryjoin="Tag.id == CategoryExclusion.tag_id",
secondaryjoin="Tag.id == CategoryExclusion.category_id",
@@ -140,14 +140,14 @@ def __init__(
id: int | None = None,
shorthand: str | None = None,
aliases: set[TagAlias] | None = None,
- parent_tags: set["Tag"] | None = None,
+ parent_tags: set[Tag] | None = None,
icon: str | None = None,
color_namespace: str | None = None,
color_slug: str | None = None,
disambiguation_id: int | None = None,
is_category: bool = False,
is_hidden: bool = False,
- category_exclusions: set["Tag"] | None = None,
+ category_exclusions: set[Tag] | None = None,
):
self.name = name
self.aliases = aliases or set()
@@ -181,16 +181,16 @@ def __eq__(self, value: object) -> bool:
return False
return self.id == value.id
- def __lt__(self, other: "Tag") -> bool:
+ def __lt__(self, other: Tag) -> bool:
return self.name < other.name
- def __le__(self, other: "Tag") -> bool:
+ def __le__(self, other: Tag) -> bool:
return self.name <= other.name
- def __gt__(self, other: "Tag") -> bool:
+ def __gt__(self, other: Tag) -> bool:
return self.name > other.name
- def __ge__(self, other: "Tag") -> bool:
+ def __ge__(self, other: Tag) -> bool:
return self.name >= other.name
diff --git a/src/tagstudio/core/library/json/library.py b/src/tagstudio/core/library/json/library.py
index e3370e2a2..46f09f47e 100644
--- a/src/tagstudio/core/library/json/library.py
+++ b/src/tagstudio/core/library/json/library.py
@@ -861,7 +861,7 @@ def refresh_dir(self) -> Generator:
self.files_not_in_library,
key=lambda t: -(self.library_dir / t).stat().st_ctime,
)
- except (FileExistsError, FileNotFoundError):
+ except FileExistsError, FileNotFoundError:
print(
"[LIBRARY] [ERROR] Couldn't sort files, some were moved during the scanning/sorting process."
)
diff --git a/src/tagstudio/core/query_lang/ast.py b/src/tagstudio/core/query_lang/ast.py
index c8268ea48..25ebfcb6b 100644
--- a/src/tagstudio/core/query_lang/ast.py
+++ b/src/tagstudio/core/query_lang/ast.py
@@ -16,7 +16,7 @@ class ConstraintType(Enum):
Special = 5
@staticmethod
- def from_string(text: str) -> "ConstraintType | None":
+ def from_string(text: str) -> ConstraintType | None:
return {
"tag": ConstraintType.Tag,
"tag_id": ConstraintType.TagID,
@@ -28,7 +28,7 @@ def from_string(text: str) -> "ConstraintType | None":
class AST:
- parent: "AST | None" = None
+ parent: AST | None = None
@override
def __str__(self):
@@ -65,9 +65,9 @@ def __init__(self, elements: list[AST]) -> None:
class Constraint(AST):
type: ConstraintType
value: str
- properties: list["Property"]
+ properties: list[Property]
- def __init__(self, type: ConstraintType, value: str, properties: list["Property"]) -> None:
+ def __init__(self, type: ConstraintType, value: str, properties: list[Property]) -> None:
super().__init__()
for prop in properties:
prop.parent = self
diff --git a/src/tagstudio/core/query_lang/tokenizer.py b/src/tagstudio/core/query_lang/tokenizer.py
index fc6e66851..45b04a096 100644
--- a/src/tagstudio/core/query_lang/tokenizer.py
+++ b/src/tagstudio/core/query_lang/tokenizer.py
@@ -39,11 +39,11 @@ def __init__(
self.end = end
@staticmethod
- def from_type(type: TokenType, pos: int) -> "Token":
+ def from_type(type: TokenType, pos: int) -> Token:
return Token(type, None, pos, pos)
@staticmethod
- def EOF(pos: int) -> "Token": # noqa: N802
+ def EOF(pos: int) -> Token: # noqa: N802
return Token.from_type(TokenType.EOF, pos)
@override
diff --git a/src/tagstudio/core/utils/str_formatting.py b/src/tagstudio/core/utils/str_formatting.py
index 2f0f544d5..3e8861cec 100644
--- a/src/tagstudio/core/utils/str_formatting.py
+++ b/src/tagstudio/core/utils/str_formatting.py
@@ -58,5 +58,5 @@ def format_duration(duration: int | float) -> str:
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
return f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes}:{seconds:02}"
- except (OverflowError, ValueError):
+ except OverflowError, ValueError:
return "-:--"
diff --git a/src/tagstudio/i18n/translations.py b/src/tagstudio/i18n/translations.py
index 7c9c0195f..23ca1d6c4 100644
--- a/src/tagstudio/i18n/translations.py
+++ b/src/tagstudio/i18n/translations.py
@@ -86,7 +86,7 @@ def change_language(self, lang: str):
def __format(self, text: str, **kwargs: ...) -> str:
try:
return text.format(**kwargs)
- except (KeyError, ValueError):
+ except KeyError, ValueError:
logger.error(
"[Translations] Error while formatting translation.",
text=text,
diff --git a/src/tagstudio/previews/renderers/archive.py b/src/tagstudio/previews/renderers/archive.py
index d2918e513..418d1e38b 100644
--- a/src/tagstudio/previews/renderers/archive.py
+++ b/src/tagstudio/previews/renderers/archive.py
@@ -52,7 +52,7 @@ def namelist(self) -> list[str]:
def read(self, name: str) -> bytes:
return unwrap(self.tar.extractfile(name)).read()
- def __enter__(self) -> "TarFile":
+ def __enter__(self) -> TarFile:
self.tar = tarfile.open(name=self.filepath, mode=self.mode).__enter__()
return self
diff --git a/src/tagstudio/qt/app_settings.py b/src/tagstudio/qt/app_settings.py
index 5817e4a65..450fa788e 100644
--- a/src/tagstudio/qt/app_settings.py
+++ b/src/tagstudio/qt/app_settings.py
@@ -88,7 +88,7 @@ class AppSettings(BaseModel):
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)
@staticmethod
- def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "AppSettings":
+ def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> AppSettings:
if path.exists():
with open(path) as file:
filecontents = file.read()
diff --git a/src/tagstudio/qt/controllers/fix_ignored_modal.py b/src/tagstudio/qt/controllers/fix_ignored_modal.py
index 341469e04..4704a7f81 100644
--- a/src/tagstudio/qt/controllers/fix_ignored_modal.py
+++ b/src/tagstudio/qt/controllers/fix_ignored_modal.py
@@ -24,7 +24,7 @@
# TODO: Use newer MVC style guidelines
class FixIgnoredEntriesModal(FixIgnoredEntriesModalView):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__(library, driver)
self.tracker = IgnoredRegistry(self.lib)
diff --git a/src/tagstudio/qt/controllers/inspector.py b/src/tagstudio/qt/controllers/inspector.py
index b20871be8..4e98c5464 100644
--- a/src/tagstudio/qt/controllers/inspector.py
+++ b/src/tagstudio/qt/controllers/inspector.py
@@ -45,7 +45,7 @@ class _ItemMode(IntEnum):
class Inspector(QWidget):
- def __init__(self, driver: "QtDriver") -> None:
+ def __init__(self, driver: QtDriver) -> None:
super().__init__()
self._driver = driver
self._lib = self._driver.lib
diff --git a/src/tagstudio/qt/controllers/library_info_window.py b/src/tagstudio/qt/controllers/library_info_window.py
index 9dddd0346..1f18cfac3 100644
--- a/src/tagstudio/qt/controllers/library_info_window.py
+++ b/src/tagstudio/qt/controllers/library_info_window.py
@@ -33,7 +33,7 @@
# TODO: Use newer MVC style guidelines
class LibraryInfoWindow(LibraryInfoWindowView):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__(library, driver)
# Statistics Buttons
diff --git a/src/tagstudio/qt/controllers/main_window.py b/src/tagstudio/qt/controllers/main_window.py
index 6bf6b9fb0..98eeb4bad 100644
--- a/src/tagstudio/qt/controllers/main_window.py
+++ b/src/tagstudio/qt/controllers/main_window.py
@@ -458,7 +458,7 @@ class MainWindow(QMainWindow):
(Translations["home.thumbnail_size.mini"], 76),
]
- def __init__(self, driver: "QtDriver", parent: QWidget | None = None) -> None:
+ def __init__(self, driver: QtDriver, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.rm = ResourceManager()
@@ -529,7 +529,7 @@ def setup_menu_bar(self):
# endregion
- def setup_central_widget(self, driver: "QtDriver"):
+ def setup_central_widget(self, driver: QtDriver):
self.central_widget = QWidget(self)
self.central_widget.setObjectName("central_widget")
self.central_layout = QGridLayout(self.central_widget)
@@ -651,7 +651,7 @@ def setup_extra_input_bar(self):
self.central_layout.addLayout(self.extra_input_layout, 5, 0, 1, 1)
- def setup_content(self, driver: "QtDriver"):
+ def setup_content(self, driver: QtDriver):
self.content_layout = QHBoxLayout()
self.content_layout.setObjectName("content_layout")
@@ -667,7 +667,7 @@ def setup_content(self, driver: "QtDriver"):
self.central_layout.addLayout(self.content_layout, 10, 0, 1, 1)
- def setup_entry_list(self, driver: "QtDriver"):
+ def setup_entry_list(self, driver: QtDriver):
self.entry_list_container = QWidget()
self.entry_list_layout = QVBoxLayout(self.entry_list_container)
self.entry_list_layout.setSpacing(0)
@@ -700,7 +700,7 @@ def setup_entry_list(self, driver: "QtDriver"):
self.entry_list_layout.addWidget(self.pagination)
self.content_splitter.addWidget(self.entry_list_container)
- def setup_preview_panel(self, driver: "QtDriver"):
+ def setup_preview_panel(self, driver: QtDriver):
self.preview_panel = Inspector(driver)
self.content_splitter.addWidget(self.preview_panel)
diff --git a/src/tagstudio/qt/controllers/merge_dupe_entries_progress.py b/src/tagstudio/qt/controllers/merge_dupe_entries_progress.py
index d7973379c..db73ea176 100644
--- a/src/tagstudio/qt/controllers/merge_dupe_entries_progress.py
+++ b/src/tagstudio/qt/controllers/merge_dupe_entries_progress.py
@@ -18,7 +18,7 @@
class MergeDuplicateEntriesProgress(QObject):
done = Signal()
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.lib = library
self.driver = driver
diff --git a/src/tagstudio/qt/controllers/preview_thumb.py b/src/tagstudio/qt/controllers/preview_thumb.py
index d20723a28..8e99d2df0 100644
--- a/src/tagstudio/qt/controllers/preview_thumb.py
+++ b/src/tagstudio/qt/controllers/preview_thumb.py
@@ -33,7 +33,7 @@
# TODO: Use newer MVC style guidelines
class PreviewThumb(PreviewThumbView):
- def __init__(self, library: Library, driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__(library, driver)
self.__driver: QtDriver = driver
diff --git a/src/tagstudio/qt/controllers/search_panel.py b/src/tagstudio/qt/controllers/search_panel.py
index 9518ac1e2..0a34b4dca 100644
--- a/src/tagstudio/qt/controllers/search_panel.py
+++ b/src/tagstudio/qt/controllers/search_panel.py
@@ -78,7 +78,7 @@ def __init__(
self.setMinimumSize(300, 400)
self.connect_callbacks(self)
- def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
+ def connect_callbacks(self, controller: SearchPanel[Any]) -> None: # pyright: ignore[reportExplicitAny]
self.layout().limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
self.layout().search_field.textChanged.connect(controller.on_search_query_changed)
self.layout().search_field.returnPressed.connect(
diff --git a/src/tagstudio/qt/controllers/tag_box.py b/src/tagstudio/qt/controllers/tag_box.py
index a6bdcbd8c..f174522ef 100644
--- a/src/tagstudio/qt/controllers/tag_box.py
+++ b/src/tagstudio/qt/controllers/tag_box.py
@@ -28,7 +28,7 @@ class TagBoxWidget(TagBoxWidgetView):
__entries: list[int] = []
- def __init__(self, title: str, driver: "QtDriver"):
+ def __init__(self, title: str, driver: QtDriver):
super().__init__(title, driver)
self.__driver = driver
diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/mixed/color_box.py
index 39a0577d7..71b83b3ff 100644
--- a/src/tagstudio/qt/mixed/color_box.py
+++ b/src/tagstudio/qt/mixed/color_box.py
@@ -33,8 +33,8 @@ class ColorBoxWidget(FieldWidget):
def __init__(
self,
group: str,
- colors: list["TagColorGroup"],
- library: "Library",
+ colors: list[TagColorGroup],
+ library: Library,
) -> None:
self.namespace = group
self.colors: list[TagColorGroup] = colors
diff --git a/src/tagstudio/qt/mixed/datetime_picker.py b/src/tagstudio/qt/mixed/datetime_picker.py
index cca8e91fe..ef5bb4cf9 100644
--- a/src/tagstudio/qt/mixed/datetime_picker.py
+++ b/src/tagstudio/qt/mixed/datetime_picker.py
@@ -41,7 +41,7 @@ def qdtf2dtf(dtf: str) -> str:
# TODO: Split to use MVC guidelines.
class DatetimePicker(ModalContent):
- def __init__(self, driver: "QtDriver", name: str, datetime: dt | str):
+ def __init__(self, driver: QtDriver, name: str, datetime: dt | str):
super().__init__()
self.setMinimumSize(300, 60)
self.root_layout = QVBoxLayout(self)
diff --git a/src/tagstudio/qt/mixed/drop_import_modal.py b/src/tagstudio/qt/mixed/drop_import_modal.py
index df2da028b..859097584 100644
--- a/src/tagstudio/qt/mixed/drop_import_modal.py
+++ b/src/tagstudio/qt/mixed/drop_import_modal.py
@@ -34,7 +34,7 @@ class DuplicateChoice(enum.StrEnum):
class DropImportModal(QWidget):
DUPE_NAME_LIMT: int = 5
- def __init__(self, driver: "QtDriver"):
+ def __init__(self, driver: QtDriver):
super().__init__()
self.driver: QtDriver = driver
diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py
index 2997731c9..daf57109a 100644
--- a/src/tagstudio/qt/mixed/field_containers.py
+++ b/src/tagstudio/qt/mixed/field_containers.py
@@ -50,7 +50,7 @@ class FieldContainers(QWidget):
on_tags_update = Signal()
- def __init__(self, library: Library, driver: "QtDriver") -> None:
+ def __init__(self, library: Library, driver: QtDriver) -> None:
super().__init__()
self.lib = library
@@ -321,7 +321,7 @@ def write_datetime_container(
text = self.driver.settings.format_datetime(
DatetimePicker.string2dt(field.value)
)
- except (ValueError, AssertionError):
+ except ValueError, AssertionError:
text = str(field.value)
else:
text = f"{Translations['field.mixed_data']}"
diff --git a/src/tagstudio/qt/mixed/field_widget.py b/src/tagstudio/qt/mixed/field_widget.py
index 914ab7f69..aceb4e7fb 100644
--- a/src/tagstudio/qt/mixed/field_widget.py
+++ b/src/tagstudio/qt/mixed/field_widget.py
@@ -136,7 +136,7 @@ def set_remove_callback(self, callback: Callable[[], None] | None = None) -> Non
if callback:
self.remove_button.clicked.connect(callback)
- def set_inner_widget(self, widget: "FieldWidget") -> None:
+ def set_inner_widget(self, widget: FieldWidget) -> None:
if self.field_layout.itemAt(0):
old: QWidget = self.field_layout.itemAt(0).widget()
self.field_layout.removeWidget(old)
diff --git a/src/tagstudio/qt/mixed/file_attributes.py b/src/tagstudio/qt/mixed/file_attributes.py
index 9409bb66c..e37dfba09 100644
--- a/src/tagstudio/qt/mixed/file_attributes.py
+++ b/src/tagstudio/qt/mixed/file_attributes.py
@@ -42,7 +42,7 @@ class FileAttributeData:
# TODO: Split to use MVC guidelines.
class FileAttributes(QWidget):
- def __init__(self, library: Library, driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
root_layout = QVBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
diff --git a/src/tagstudio/qt/mixed/fix_dupe_files.py b/src/tagstudio/qt/mixed/fix_dupe_files.py
index cc275c449..cfd5f48fc 100644
--- a/src/tagstudio/qt/mixed/fix_dupe_files.py
+++ b/src/tagstudio/qt/mixed/fix_dupe_files.py
@@ -28,7 +28,7 @@
# TODO: Split to use MVC guidelines.
class FixDupeFilesModal(QWidget):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.lib = library
self.driver = driver
diff --git a/src/tagstudio/qt/mixed/fix_unlinked.py b/src/tagstudio/qt/mixed/fix_unlinked.py
index 56237134d..39cad7c79 100644
--- a/src/tagstudio/qt/mixed/fix_unlinked.py
+++ b/src/tagstudio/qt/mixed/fix_unlinked.py
@@ -24,7 +24,7 @@
# TODO: Split to use MVC guidelines.
class FixUnlinkedEntriesModal(QWidget):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.lib = library
self.driver = driver
diff --git a/src/tagstudio/qt/mixed/folders_to_tags.py b/src/tagstudio/qt/mixed/folders_to_tags.py
index 45908b0bd..b3dd8b3ce 100644
--- a/src/tagstudio/qt/mixed/folders_to_tags.py
+++ b/src/tagstudio/qt/mixed/folders_to_tags.py
@@ -6,6 +6,7 @@
from collections.abc import Sequence
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, override
+from warnings import deprecated
import structlog
from PySide6 import QtCore, QtGui
@@ -19,7 +20,6 @@
QVBoxLayout,
QWidget,
)
-from typing_extensions import deprecated
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from tagstudio.core.library.alchemy.enums import TagColorEnum
@@ -39,7 +39,7 @@
@dataclass
class BranchData:
- dirs: dict[str, "BranchData"] = field(default_factory=dict)
+ dirs: dict[str, BranchData] = field(default_factory=dict)
files: list[str] = field(default_factory=list)
tag: Tag | None = None
@@ -165,7 +165,7 @@ def cut_branches_adding_nothing(branch: BranchData) -> bool:
class FoldersToTagsModal(QWidget):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.library = library
self.driver = driver
diff --git a/src/tagstudio/qt/mixed/item_thumb.py b/src/tagstudio/qt/mixed/item_thumb.py
index 6ebf87079..b89a2f205 100644
--- a/src/tagstudio/qt/mixed/item_thumb.py
+++ b/src/tagstudio/qt/mixed/item_thumb.py
@@ -96,7 +96,7 @@ def __init__(
self,
mode: ItemType | None,
library: Library,
- driver: "QtDriver",
+ driver: QtDriver,
thumb_size: tuple[int, int],
show_filename_label: bool = False,
):
@@ -440,7 +440,7 @@ def update_size(self, size: QSize):
self.thumb_button.setMinimumSize(size)
self.thumb_button.setMaximumSize(size)
- def set_item(self, entry: "Entry"):
+ def set_item(self, entry: Entry):
self.set_item_id(entry.id)
path = unwrap(self.lib.library_dir) / entry.path
self.set_item_path(path)
diff --git a/src/tagstudio/qt/mixed/landing.py b/src/tagstudio/qt/mixed/landing.py
index a738ac0f7..30d042a51 100644
--- a/src/tagstudio/qt/mixed/landing.py
+++ b/src/tagstudio/qt/mixed/landing.py
@@ -29,7 +29,7 @@ class LandingWidget(QWidget):
mono_logo: Image.Image = rm.ts_logo_text_mono
color_logo: Image.Image = rm.ts_logo_text_color
- def __init__(self, driver: "QtDriver", pixel_ratio: float):
+ def __init__(self, driver: QtDriver, pixel_ratio: float):
super().__init__()
self.driver = driver
self.logo_label: ClickableLabel = ClickableLabel()
diff --git a/src/tagstudio/qt/mixed/media_player.py b/src/tagstudio/qt/mixed/media_player.py
index 7de453594..a8029a311 100644
--- a/src/tagstudio/qt/mixed/media_player.py
+++ b/src/tagstudio/qt/mixed/media_player.py
@@ -53,9 +53,9 @@ class MediaPlayer(QGraphicsView):
Gives a basic control set to manage media playback.
"""
- video_preview: "VideoPreview | None" = None
+ video_preview: VideoPreview | None = None
- def __init__(self, driver: "QtDriver") -> None:
+ def __init__(self, driver: QtDriver) -> None:
super().__init__()
self.driver = driver
self.play_icon = QPixmap.fromImage(
diff --git a/src/tagstudio/qt/mixed/migration_modal.py b/src/tagstudio/qt/mixed/migration_modal.py
index 65457e106..43edc8947 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
+from warnings import deprecated
import structlog
import wcmatch.fnmatch as fnmatch
@@ -23,7 +24,6 @@
)
from sqlalchemy import select
from sqlalchemy.orm import Session
-from typing_extensions import deprecated
from tagstudio.core.constants import (
IGNORE_NAME,
diff --git a/src/tagstudio/qt/mixed/mirror_entries_modal.py b/src/tagstudio/qt/mixed/mirror_entries_modal.py
index 66b964ce5..3ec9ea611 100644
--- a/src/tagstudio/qt/mixed/mirror_entries_modal.py
+++ b/src/tagstudio/qt/mixed/mirror_entries_modal.py
@@ -22,7 +22,7 @@
class MirrorEntriesModal(QWidget):
done = Signal()
- def __init__(self, driver: "QtDriver", tracker: DupeFilesRegistry):
+ def __init__(self, driver: QtDriver, tracker: DupeFilesRegistry):
super().__init__()
self.driver = driver
self.setWindowTitle(Translations["entries.mirror.window_title"])
diff --git a/src/tagstudio/qt/mixed/remove_ignored_modal.py b/src/tagstudio/qt/mixed/remove_ignored_modal.py
index 0870770ec..3787ba75d 100644
--- a/src/tagstudio/qt/mixed/remove_ignored_modal.py
+++ b/src/tagstudio/qt/mixed/remove_ignored_modal.py
@@ -22,7 +22,7 @@
class RemoveIgnoredModal(QWidget):
done = Signal()
- def __init__(self, driver: "QtDriver", tracker: IgnoredRegistry):
+ def __init__(self, driver: QtDriver, tracker: IgnoredRegistry):
super().__init__()
self.driver = driver
self.tracker = tracker
diff --git a/src/tagstudio/qt/mixed/remove_unlinked_modal.py b/src/tagstudio/qt/mixed/remove_unlinked_modal.py
index ff6bbcf13..d34fa02b9 100644
--- a/src/tagstudio/qt/mixed/remove_unlinked_modal.py
+++ b/src/tagstudio/qt/mixed/remove_unlinked_modal.py
@@ -22,7 +22,7 @@
class RemoveUnlinkedEntriesModal(QWidget):
done = Signal()
- def __init__(self, driver: "QtDriver", tracker: UnlinkedRegistry):
+ def __init__(self, driver: QtDriver, tracker: UnlinkedRegistry):
super().__init__()
self.driver = driver
self.tracker = tracker
diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py
index 3a104398c..393616b03 100644
--- a/src/tagstudio/qt/mixed/settings_panel.py
+++ b/src/tagstudio/qt/mixed/settings_panel.py
@@ -41,7 +41,7 @@
# TODO: Split to use MVC guidelines.
class SettingsPanel(ModalContent):
- driver: "QtDriver"
+ driver: QtDriver
filepath_option_map: dict[ShowFilepathOption, str] = {
ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"],
@@ -88,7 +88,7 @@ class SettingsPanel(ModalContent):
"%Y.%m.%d": "2024.08.21",
}
- def __init__(self, driver: "QtDriver"):
+ def __init__(self, driver: QtDriver):
super().__init__()
# set these "constants" because language will be loaded from config shortly after startup
# and we want to use the current language for the dropdowns
@@ -400,7 +400,7 @@ def get_settings(self) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
"splash": self.splash_combobox.currentData(),
}
- def update_settings(self, driver: "QtDriver"):
+ def update_settings(self, driver: QtDriver):
settings = self.get_settings()
driver.settings.language = settings["language"]
@@ -440,7 +440,7 @@ def update_settings(self, driver: "QtDriver"):
)
@classmethod
- def build_modal(cls, driver: "QtDriver") -> Modal:
+ def build_modal(cls, driver: QtDriver) -> Modal:
settings_panel = cls(driver)
modal = Modal(
diff --git a/src/tagstudio/qt/mixed/tag_color_label.py b/src/tagstudio/qt/mixed/tag_color_label.py
index bcbea8866..abb091002 100644
--- a/src/tagstudio/qt/mixed/tag_color_label.py
+++ b/src/tagstudio/qt/mixed/tag_color_label.py
@@ -41,7 +41,7 @@ def __init__(
color: TagColorGroup | None,
has_edit: bool,
has_remove: bool,
- library: "Library | None" = None,
+ library: Library | None = None,
) -> None:
super().__init__()
self.color = color
diff --git a/src/tagstudio/qt/mixed/tag_color_manager.py b/src/tagstudio/qt/mixed/tag_color_manager.py
index 12df1b4f3..f0c1a15f7 100644
--- a/src/tagstudio/qt/mixed/tag_color_manager.py
+++ b/src/tagstudio/qt/mixed/tag_color_manager.py
@@ -42,7 +42,7 @@ class TagColorManager(QWidget):
def __init__(
self,
- driver: "QtDriver",
+ driver: QtDriver,
):
super().__init__()
self.driver = driver
diff --git a/src/tagstudio/qt/mixed/tag_color_preview.py b/src/tagstudio/qt/mixed/tag_color_preview.py
index 098569e2c..7ad40e655 100644
--- a/src/tagstudio/qt/mixed/tag_color_preview.py
+++ b/src/tagstudio/qt/mixed/tag_color_preview.py
@@ -32,7 +32,7 @@ class TagColorPreview(QWidget):
def __init__(
self,
- library: "Library",
+ library: Library,
tag_color_group: TagColorGroup | None,
) -> None:
super().__init__()
diff --git a/src/tagstudio/qt/mixed/tag_widget.py b/src/tagstudio/qt/mixed/tag_widget.py
index c65ab366f..05b9ca28e 100644
--- a/src/tagstudio/qt/mixed/tag_widget.py
+++ b/src/tagstudio/qt/mixed/tag_widget.py
@@ -111,7 +111,7 @@ class TagWidget(QWidget):
tag: Tag | None
def __init__(
- self, tag: Tag | None, has_edit: bool, has_remove: bool, library: "Library | None" = None
+ self, tag: Tag | None, has_edit: bool, has_remove: bool, library: Library | None = None
) -> None:
super().__init__()
self.tag = tag
diff --git a/src/tagstudio/qt/resource_manager.py b/src/tagstudio/qt/resource_manager.py
index d769a4ce9..7eb28ddfa 100644
--- a/src/tagstudio/qt/resource_manager.py
+++ b/src/tagstudio/qt/resource_manager.py
@@ -20,7 +20,7 @@ class ResourceManager:
_map: dict[str, dict[str, str]] = {}
_cache: dict[str, bytes | str | Image.Image | QPixmap] = {}
- _instance: "ResourceManager | None" = None
+ _instance: ResourceManager | None = None
def __new__(cls):
if ResourceManager._instance is None:
diff --git a/src/tagstudio/qt/views/fix_ignored_modal_view.py b/src/tagstudio/qt/views/fix_ignored_modal_view.py
index a164c4a21..1e9d92242 100644
--- a/src/tagstudio/qt/views/fix_ignored_modal_view.py
+++ b/src/tagstudio/qt/views/fix_ignored_modal_view.py
@@ -18,7 +18,7 @@
# TODO: Use newer MVC style guidelines
class FixIgnoredEntriesModalView(QWidget):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.lib = library
self.driver = driver
diff --git a/src/tagstudio/qt/views/inspector_view.py b/src/tagstudio/qt/views/inspector_view.py
index a0732ac12..3195d0e26 100644
--- a/src/tagstudio/qt/views/inspector_view.py
+++ b/src/tagstudio/qt/views/inspector_view.py
@@ -28,7 +28,7 @@
class InspectorView(QVBoxLayout):
- def __init__(self, driver: "QtDriver", pixel_ratio: float) -> None:
+ def __init__(self, driver: QtDriver, pixel_ratio: float) -> None:
super().__init__()
self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(6)
diff --git a/src/tagstudio/qt/views/layouts/thumb_grid_layout.py b/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
index 12c2e68fa..a3b44b4c6 100644
--- a/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
+++ b/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
@@ -27,7 +27,7 @@ class ThumbGridLayout(QLayout):
# Id of first visible entry
visible_changed = Signal(int)
- def __init__(self, driver: "QtDriver", scroll_area: QScrollArea) -> None:
+ def __init__(self, driver: QtDriver, scroll_area: QScrollArea) -> None:
super().__init__(None)
self.driver: QtDriver = driver
self.scroll_area: QScrollArea = scroll_area
diff --git a/src/tagstudio/qt/views/library_info_window_view.py b/src/tagstudio/qt/views/library_info_window_view.py
index 46cecd6c0..623066edd 100644
--- a/src/tagstudio/qt/views/library_info_window_view.py
+++ b/src/tagstudio/qt/views/library_info_window_view.py
@@ -34,7 +34,7 @@
# TODO: Use newer MVC style guidelines
class LibraryInfoWindowView(QWidget):
- def __init__(self, library: "Library", driver: "QtDriver"):
+ def __init__(self, library: Library, driver: QtDriver):
super().__init__()
self.lib = library
self.driver = driver
diff --git a/src/tagstudio/qt/views/preview_thumb_view.py b/src/tagstudio/qt/views/preview_thumb_view.py
index f3ca286a3..5fcb922ff 100644
--- a/src/tagstudio/qt/views/preview_thumb_view.py
+++ b/src/tagstudio/qt/views/preview_thumb_view.py
@@ -44,7 +44,7 @@ class PreviewThumbView(QWidget):
__should_render_on_resize: bool
__rendered_res: tuple[int, int]
- def __init__(self, library: Library, driver: "QtDriver") -> None:
+ def __init__(self, library: Library, driver: QtDriver) -> None:
super().__init__()
self._driver = driver
diff --git a/src/tagstudio/qt/views/tag_box_view.py b/src/tagstudio/qt/views/tag_box_view.py
index 0d3ddde1a..f66829a2a 100644
--- a/src/tagstudio/qt/views/tag_box_view.py
+++ b/src/tagstudio/qt/views/tag_box_view.py
@@ -23,7 +23,7 @@
class TagBoxWidgetView(FieldWidget):
__lib: Library
- def __init__(self, title: str, driver: "QtDriver") -> None:
+ def __init__(self, title: str, driver: QtDriver) -> None:
super().__init__(title)
self.__lib = driver.lib
From 8ec26dea3c372c87bc5a3584ff8515c19f17a1da Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Tue, 18 Aug 2026 14:34:41 -0700
Subject: [PATCH 5/7] fix: disallow 3.15.0
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index db2a3ac9a..d6d575e78 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ description = "A User-Focused Photo & File Management System."
version = "9.6.4"
license = "GPL-3.0-only"
readme = "README.md"
-requires-python = ">=3.14,<=3.15"
+requires-python = ">=3.14,<3.15"
dependencies = [
"audioop-lts~=0.2.2",
"chardet~=5.2",
From 291d4b76780c1a609eb647677f6e13447a072403 Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Tue, 18 Aug 2026 14:38:08 -0700
Subject: [PATCH 6/7] build: bump pyside version to 6.11.2
---
pyproject.toml | 2 +-
src/tagstudio/previews/renderers/audio.py | 2 +-
.../qt/controllers/autofill_line_edit.py | 2 +-
.../qt/controllers/field_suggest_box.py | 4 +-
.../field_template_search_panel.py | 4 +-
src/tagstudio/qt/controllers/progress_bar.py | 8 ++--
src/tagstudio/qt/controllers/search_panel.py | 2 +-
src/tagstudio/qt/controllers/suggest_box.py | 11 ++++--
.../qt/controllers/tag_search_panel.py | 4 +-
.../qt/controllers/tag_suggest_box.py | 4 +-
.../qt/controllers/underlined_widget.py | 3 +-
src/tagstudio/qt/mixed/build_tag.py | 4 +-
src/tagstudio/qt/mixed/color_box.py | 2 +-
src/tagstudio/qt/mixed/field_widget.py | 11 ++++--
src/tagstudio/qt/mixed/file_attributes.py | 6 +--
src/tagstudio/qt/mixed/folders_to_tags.py | 1 +
src/tagstudio/qt/mixed/migration_modal.py | 37 ++++++++-----------
src/tagstudio/qt/mixed/paged_panel.py | 5 ++-
src/tagstudio/qt/mixed/pagination.py | 1 +
src/tagstudio/qt/mixed/tag_color_manager.py | 4 +-
src/tagstudio/qt/views/layouts/flow_layout.py | 6 ++-
.../qt/views/layouts/thumb_grid_layout.py | 2 +-
tests/qt/test_build_tag_panel.py | 1 +
23 files changed, 72 insertions(+), 54 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index d6d575e78..a0ec9bb53 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,7 +27,7 @@ dependencies = [
"py7zr~=1.1.3",
"pydantic~=2.10",
"pydub~=0.25",
- "PySide6==6.8.0.*",
+ "PySide6==6.11.2",
"rarfile==4.2",
"rawpy~=0.27",
"Send2Trash>=1.8,<3",
diff --git a/src/tagstudio/previews/renderers/audio.py b/src/tagstudio/previews/renderers/audio.py
index 262e484c1..6b981a7ee 100644
--- a/src/tagstudio/previews/renderers/audio.py
+++ b/src/tagstudio/previews/renderers/audio.py
@@ -52,7 +52,7 @@ def audio_album_thumb(filepath: Path, ext: str) -> Image.Image | None:
image = artwork
except (
FileNotFoundError,
- id3.ID3NoHeaderError, # pyright: ignore[reportPrivateImportUsage]
+ id3.ID3NoHeaderError,
mp4.MP4MetadataError,
mp4.MP4StreamInfoError,
MutagenError,
diff --git a/src/tagstudio/qt/controllers/autofill_line_edit.py b/src/tagstudio/qt/controllers/autofill_line_edit.py
index dfc5a451c..1e4812582 100644
--- a/src/tagstudio/qt/controllers/autofill_line_edit.py
+++ b/src/tagstudio/qt/controllers/autofill_line_edit.py
@@ -77,4 +77,4 @@ def show_action_menu(self, pos: QPoint) -> None:
# Filter out icon action(s)
if action.text():
menu.addAction(action)
- menu.exec(self.mapToGlobal(pos)) # pyright: ignore[reportArgumentType]
+ menu.exec(self.mapToGlobal(pos))
diff --git a/src/tagstudio/qt/controllers/field_suggest_box.py b/src/tagstudio/qt/controllers/field_suggest_box.py
index 199de7f3c..e1d3efcc4 100644
--- a/src/tagstudio/qt/controllers/field_suggest_box.py
+++ b/src/tagstudio/qt/controllers/field_suggest_box.py
@@ -11,6 +11,7 @@
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.app_settings import AppSettings
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
@@ -155,6 +156,7 @@ def _get_item_widget(self, index: int, library: Library | None) -> UnderlinedWid
widget.setHidden(True)
self.layout().content_layout.addWidget(widget)
- widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
+ item = unwrap(self.layout().content_layout.itemAt(index))
+ widget_: QWidget = unwrap(item.widget())
assert isinstance(widget_, UnderlinedWidget)
return widget_
diff --git a/src/tagstudio/qt/controllers/field_template_search_panel.py b/src/tagstudio/qt/controllers/field_template_search_panel.py
index d131b3f43..f1b4c9195 100644
--- a/src/tagstudio/qt/controllers/field_template_search_panel.py
+++ b/src/tagstudio/qt/controllers/field_template_search_panel.py
@@ -11,6 +11,7 @@
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
from tagstudio.qt.controllers.field_template_widget import FieldTemplateWidget
@@ -173,6 +174,7 @@ def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateW
pad_field_template_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_field_template_widget)
- field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
+ item = unwrap(self.layout().scroll_layout.itemAt(index))
+ field_template_widget: QWidget = unwrap(item.widget())
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
diff --git a/src/tagstudio/qt/controllers/progress_bar.py b/src/tagstudio/qt/controllers/progress_bar.py
index 98b5fc7b2..dd2618f76 100644
--- a/src/tagstudio/qt/controllers/progress_bar.py
+++ b/src/tagstudio/qt/controllers/progress_bar.py
@@ -27,10 +27,10 @@ def __init__(
super().__init__()
self.root = QVBoxLayout(self)
self.pb = QProgressDialog(
- labelText=label_text,
- minimum=minimum,
- cancelButtonText=cancel_button_text, # pyright: ignore[reportArgumentType]
- maximum=maximum,
+ label_text,
+ cancel_button_text or "",
+ minimum,
+ maximum,
)
self.root.addWidget(self.pb)
self.setFixedSize(432, 112)
diff --git a/src/tagstudio/qt/controllers/search_panel.py b/src/tagstudio/qt/controllers/search_panel.py
index 0a34b4dca..96f922574 100644
--- a/src/tagstudio/qt/controllers/search_panel.py
+++ b/src/tagstudio/qt/controllers/search_panel.py
@@ -166,7 +166,7 @@ def on_search_query_submitted(self, query: str) -> None:
if not query:
self.layout().search_field.setFocus()
parent: QWidget | None = self.parentWidget()
- if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
+ if parent is not None:
parent.hide()
return
diff --git a/src/tagstudio/qt/controllers/suggest_box.py b/src/tagstudio/qt/controllers/suggest_box.py
index b8c33160b..9627aa04e 100644
--- a/src/tagstudio/qt/controllers/suggest_box.py
+++ b/src/tagstudio/qt/controllers/suggest_box.py
@@ -11,6 +11,7 @@
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
from tagstudio.core.library.alchemy.library import Library
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.app_settings import AppSettings
from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui
@@ -110,7 +111,7 @@ def set_hint_icon(self, icon: Image.Image | None) -> None:
def _on_shift_held(self, held: bool) -> None:
self._is_shift_held = held
for i in range(0, self.layout().content_layout.count()):
- underlined_widget = self.layout().content_layout.itemAt(i).widget()
+ underlined_widget = self.layout().content_layout.itemAt(i).widget() # pyright: ignore
assert isinstance(underlined_widget, UnderlinedWidget)
if held and i == self._selection_index:
@@ -128,7 +129,7 @@ def _on_index_updated(self, delta: int) -> None:
# Initialize the widget count (non-hidden)
widget_count = 0
for i in range(0, self.layout().content_layout.count()):
- widget = self.layout().content_layout.itemAt(i).widget()
+ widget = unwrap(self.layout().content_layout.itemAt(i).widget()) # pyright: ignore
if not widget.isHidden():
widget_count += 1
@@ -149,7 +150,7 @@ def _on_index_updated(self, delta: int) -> None:
# Draw the correct underline for the selected widget
for i in range(0, widget_count):
- underlined_widget = self.layout().content_layout.itemAt(i).widget()
+ underlined_widget = self.layout().content_layout.itemAt(i).widget() # pyright: ignore
assert isinstance(underlined_widget, UnderlinedWidget)
if i == self._selection_index:
underlined_widget.toggle_underline(is_hidden=False)
@@ -216,7 +217,9 @@ def _update_items(self, query: str | None = None) -> None:
self._selection_index = 0
if self.layout().content_layout.count() > 0:
self.layout().scroll_area.ensureWidgetVisible(
- self.layout().content_layout.itemAt(0).widget(), xmargin=16, ymargin=0
+ self.layout().content_layout.itemAt(0).widget(), # pyright: ignore
+ xmargin=16,
+ ymargin=0,
)
# Get results for the search query
diff --git a/src/tagstudio/qt/controllers/tag_search_panel.py b/src/tagstudio/qt/controllers/tag_search_panel.py
index 1f469fa8b..7771747b6 100644
--- a/src/tagstudio/qt/controllers/tag_search_panel.py
+++ b/src/tagstudio/qt/controllers/tag_search_panel.py
@@ -12,6 +12,7 @@
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.modal_content import ModalContent
@@ -207,6 +208,7 @@ def get_item_widget(self, index: int, library: Library | None) -> TagWidget:
pad_tag_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_tag_widget)
- tag_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
+ item = unwrap(self.layout().scroll_layout.itemAt(index))
+ tag_widget: QWidget = unwrap(item.widget())
assert isinstance(tag_widget, TagWidget)
return tag_widget
diff --git a/src/tagstudio/qt/controllers/tag_suggest_box.py b/src/tagstudio/qt/controllers/tag_suggest_box.py
index d7735ba77..04789cd72 100644
--- a/src/tagstudio/qt/controllers/tag_suggest_box.py
+++ b/src/tagstudio/qt/controllers/tag_suggest_box.py
@@ -12,6 +12,7 @@
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.app_settings import AppSettings
from tagstudio.qt.controllers.modal import Modal
@@ -191,6 +192,7 @@ def _get_item_widget(self, index: int, library: Library | None) -> UnderlinedWid
widget.setHidden(True)
self.layout().content_layout.addWidget(widget)
- widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
+ item = unwrap(self.layout().content_layout.itemAt(index))
+ widget_: QWidget = unwrap(item.widget())
assert isinstance(widget_, UnderlinedWidget)
return widget_
diff --git a/src/tagstudio/qt/controllers/underlined_widget.py b/src/tagstudio/qt/controllers/underlined_widget.py
index 8c041f3b2..928758454 100644
--- a/src/tagstudio/qt/controllers/underlined_widget.py
+++ b/src/tagstudio/qt/controllers/underlined_widget.py
@@ -6,6 +6,7 @@
from PySide6.QtWidgets import QWidget
+from tagstudio.core.utils.types import unwrap
from tagstudio.qt.views.underlined_widget_view import UnderlinedWidgetView
@@ -21,7 +22,7 @@ def toggle_underline(self, is_hidden: bool) -> None:
@property
def widget(self) -> QWidget:
- return self.layout().itemAt(0).widget()
+ return unwrap(unwrap(self.layout().itemAt(0)).widget())
@override
def layout(self) -> UnderlinedWidgetView:
diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py
index 3ce3e111d..84472093d 100644
--- a/src/tagstudio/qt/mixed/build_tag.py
+++ b/src/tagstudio/qt/mixed/build_tag.py
@@ -359,7 +359,7 @@ def choose_color_callback(self, tag_color_group: TagColorGroup | None):
def set_categories(self, added_parent_id: int | None = None, removed_parent: bool = False):
while self.category_scroll_layout.itemAt(0):
- self.category_scroll_layout.takeAt(0).widget().deleteLater()
+ self.category_scroll_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
c = QWidget()
layout = QVBoxLayout(c)
@@ -480,7 +480,7 @@ def _tag_colors(self, tag: Tag) -> tuple[QColor, QColor, QColor, QColor]:
def set_parent_tags(self):
while self.parent_tags_scroll_layout.itemAt(0):
- self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
+ self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
c = QWidget()
layout = QVBoxLayout(c)
diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/mixed/color_box.py
index 71b83b3ff..03e0e64ed 100644
--- a/src/tagstudio/qt/mixed/color_box.py
+++ b/src/tagstudio/qt/mixed/color_box.py
@@ -60,7 +60,7 @@ def set_colors(self, colors: Iterable[TagColorGroup]):
color_widgets: list[TagColorLabel] = []
while self.base_layout.itemAt(0):
- unwrap(self.base_layout.takeAt(0)).widget().deleteLater()
+ unwrap(self.base_layout.takeAt(0)).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
for color in colors_:
color_widget = TagColorLabel(
diff --git a/src/tagstudio/qt/mixed/field_widget.py b/src/tagstudio/qt/mixed/field_widget.py
index aceb4e7fb..4b6996781 100644
--- a/src/tagstudio/qt/mixed/field_widget.py
+++ b/src/tagstudio/qt/mixed/field_widget.py
@@ -12,6 +12,7 @@
from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
+from tagstudio.core.utils.types import unwrap
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
from tagstudio.qt.views.styles.stylesheets import container_style, header
@@ -137,16 +138,18 @@ def set_remove_callback(self, callback: Callable[[], None] | None = None) -> Non
self.remove_button.clicked.connect(callback)
def set_inner_widget(self, widget: FieldWidget) -> None:
- if self.field_layout.itemAt(0):
- old: QWidget = self.field_layout.itemAt(0).widget()
+ item = self.field_layout.itemAt(0)
+ if item:
+ old: QWidget = unwrap(item.widget())
self.field_layout.removeWidget(old)
old.deleteLater()
self.field_layout.addWidget(widget)
def get_inner_widget(self) -> QWidget | None:
- if self.field_layout.itemAt(0):
- return self.field_layout.itemAt(0).widget()
+ item = self.field_layout.itemAt(0)
+ if item:
+ return item.widget()
return None
def set_title(self, title: str) -> None:
diff --git a/src/tagstudio/qt/mixed/file_attributes.py b/src/tagstudio/qt/mixed/file_attributes.py
index e37dfba09..43071a398 100644
--- a/src/tagstudio/qt/mixed/file_attributes.py
+++ b/src/tagstudio/qt/mixed/file_attributes.py
@@ -130,7 +130,7 @@ def update_stats(self, filepath: Path | None = None, stats: FileAttributeData |
stats = FileAttributeData()
if not filepath:
- self.layout().setSpacing(0)
+ self.layout().setSpacing(0) # pyright: ignore[reportOptionalMemberAccess]
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label.setText(f"{Translations['preview.no_selection']}")
self.file_label.set_file_path(Path())
@@ -147,7 +147,7 @@ def update_stats(self, filepath: Path | None = None, stats: FileAttributeData |
elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FILENAMES_ONLY:
display_path = Path(filepath.name)
- self.layout().setSpacing(6)
+ self.layout().setSpacing(6) # pyright: ignore[reportOptionalMemberAccess]
self.file_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.file_label.set_file_path(filepath)
self.dimensions_label.setHidden(False)
@@ -234,7 +234,7 @@ def add_newline(stats_label_text: str) -> str:
def update_multi_selection(self, count: int):
"""Format attributes for multiple selected items."""
- self.layout().setSpacing(0)
+ self.layout().setSpacing(0) # pyright: ignore[reportOptionalMemberAccess]
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label.setText(Translations.format("preview.multiple_selection", count=count))
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
diff --git a/src/tagstudio/qt/mixed/folders_to_tags.py b/src/tagstudio/qt/mixed/folders_to_tags.py
index b3dd8b3ce..97760caa0 100644
--- a/src/tagstudio/qt/mixed/folders_to_tags.py
+++ b/src/tagstudio/qt/mixed/folders_to_tags.py
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
+# pyright: reportOptionalMemberAccess=false
import math
from collections.abc import Sequence
diff --git a/src/tagstudio/qt/mixed/migration_modal.py b/src/tagstudio/qt/mixed/migration_modal.py
index 43edc8947..37aab223b 100644
--- a/src/tagstudio/qt/mixed/migration_modal.py
+++ b/src/tagstudio/qt/mixed/migration_modal.py
@@ -98,8 +98,8 @@ def init_page_info(self) -> None:
body_label = QLabel(Translations["json_migration.info.description"])
body_label.setWordWrap(True)
body_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
- body_wrapper.layout().addWidget(body_label)
- body_wrapper.layout().setContentsMargins(0, 36, 0, 0)
+ body_wrapper.layout().addWidget(body_label) # pyright: ignore[reportOptionalMemberAccess]
+ body_wrapper.layout().setContentsMargins(0, 36, 0, 0) # pyright: ignore[reportOptionalMemberAccess]
cancel_button = QPushButton(Translations["generic.cancel"])
next_button = QPushButton(Translations["generic.continue"])
@@ -287,9 +287,9 @@ def init_page_convert(self) -> None:
body_container_layout.addStretch(1)
body_container_layout.addWidget(new_lib_container)
body_container_layout.addStretch(2)
- self.body_wrapper_01.layout().addWidget(body_container)
- self.body_wrapper_01.layout().addWidget(desc_label)
- self.body_wrapper_01.layout().setSpacing(12)
+ self.body_wrapper_01.layout().addWidget(body_container) # pyright: ignore[reportOptionalMemberAccess]
+ self.body_wrapper_01.layout().addWidget(desc_label) # pyright: ignore[reportOptionalMemberAccess]
+ self.body_wrapper_01.layout().setSpacing(12) # pyright: ignore[reportOptionalMemberAccess]
back_button = QPushButton(Translations["generic.navigation.back"])
start_button = QPushButton(Translations["json_migration.start_and_preview"])
@@ -352,14 +352,9 @@ def update_json_builtins(self):
def migration_progress(self, skip_ui: bool = False):
"""Initialize the progress bar and iterator for the library migration."""
- pb = QProgressDialog(
- labelText="",
- cancelButtonText="",
- minimum=0,
- maximum=0,
- )
- pb.setCancelButton(None) # pyright: ignore[reportArgumentType]
- self.body_wrapper_01.layout().addWidget(pb)
+ pb = QProgressDialog("", "", 0, 0)
+ pb.setCancelButton(None)
+ self.body_wrapper_01.layout().addWidget(pb) # pyright: ignore[reportOptionalMemberAccess]
try:
iterator = FunctionIterator(self.migration_iterator)
@@ -480,26 +475,26 @@ def finish_migration(self):
def update_json_entry_count(self, value: int):
self.old_entry_count = value
- label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore[reportAssignmentType]
+ label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore
label.setText(self.color_value_default(value))
def update_json_tag_count(self, value: int):
self.old_tag_count = value
- label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore[reportAssignmentType]
+ label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore
label.setText(self.color_value_default(value))
def update_sql_value(self, row: int, value: int | bool, old_value: int | bool):
- label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
- warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
+ label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
+ warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
label.setText(self.color_value_conditional(old_value, value))
warning_icon.setText("" if old_value == value else self.warning)
def update_parity_value(self, row: int, value: bool):
result: str = self.match_text if value else self.differ_text
- old_label: QLabel = self.old_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
- new_label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore[reportAssignmentType]
- old_warning_icon: QLabel = self.old_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
- new_warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore[reportAssignmentType]
+ old_label: QLabel = self.old_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
+ new_label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # pyright: ignore
+ old_warning_icon: QLabel = self.old_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
+ new_warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # pyright: ignore
old_label.setText(self.color_value_conditional(self.match_text, result))
new_label.setText(self.color_value_conditional(self.match_text, result))
old_warning_icon.setText("" if value else self.warning)
diff --git a/src/tagstudio/qt/mixed/paged_panel.py b/src/tagstudio/qt/mixed/paged_panel.py
index 8c55cb625..4d356463c 100644
--- a/src/tagstudio/qt/mixed/paged_panel.py
+++ b/src/tagstudio/qt/mixed/paged_panel.py
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
-
+# pyright: reportOptionalMemberAccess=false
from typing import override
import structlog
@@ -9,6 +9,7 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
+from tagstudio.core.utils.types import unwrap
from tagstudio.qt.mixed.paged_panel_state import PagedPanelState
from tagstudio.qt.views.styles.stylesheets import header
@@ -96,7 +97,7 @@ def update_frame(self):
# Update Body Widget
if self.body_layout.itemAt(0):
self.body_layout.itemAt(0).widget().setHidden(True)
- self.body_layout.removeWidget(self.body_layout.itemAt(0).widget())
+ self.body_layout.removeWidget(unwrap(self.body_layout.itemAt(0).widget()))
self.body_layout.addWidget(frame.body_wrapper)
self.body_layout.itemAt(0).widget().setHidden(False)
diff --git a/src/tagstudio/qt/mixed/pagination.py b/src/tagstudio/qt/mixed/pagination.py
index b10be79d1..78cf7316d 100644
--- a/src/tagstudio/qt/mixed/pagination.py
+++ b/src/tagstudio/qt/mixed/pagination.py
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
+# pyright: reportOptionalMemberAccess=false
from typing import cast, override
from warnings import catch_warnings
diff --git a/src/tagstudio/qt/mixed/tag_color_manager.py b/src/tagstudio/qt/mixed/tag_color_manager.py
index f0c1a15f7..5723616ac 100644
--- a/src/tagstudio/qt/mixed/tag_color_manager.py
+++ b/src/tagstudio/qt/mixed/tag_color_manager.py
@@ -23,6 +23,7 @@
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
from tagstudio.core.enums import Theme
+from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel
@@ -166,7 +167,8 @@ def setup_color_groups(self):
def reset(self):
while self.scroll_layout.count():
- widget = self.scroll_layout.itemAt(0).widget()
+ item = unwrap(self.scroll_layout.itemAt(0))
+ widget = unwrap(item.widget())
self.scroll_layout.removeWidget(widget)
widget.deleteLater()
self.is_initialized = False
diff --git a/src/tagstudio/qt/views/layouts/flow_layout.py b/src/tagstudio/qt/views/layouts/flow_layout.py
index af14a7b2e..646998d4d 100644
--- a/src/tagstudio/qt/views/layouts/flow_layout.py
+++ b/src/tagstudio/qt/views/layouts/flow_layout.py
@@ -3,6 +3,8 @@
# SPDX-FileCopyrightText: (C) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
+# pyright: reportOptionalMemberAccess=false
+
"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x."""
@@ -44,14 +46,14 @@ def count(self) -> int:
return len(self._item_list)
@override
- def itemAt(self, index: int) -> QLayoutItem | None: # pyright: ignore[reportIncompatibleMethodOverride]
+ def itemAt(self, index: int) -> QLayoutItem | None:
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
@override
- def takeAt(self, index: int) -> QLayoutItem | None: # pyright: ignore[reportIncompatibleMethodOverride]
+ def takeAt(self, index: int) -> QLayoutItem | None:
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
diff --git a/src/tagstudio/qt/views/layouts/thumb_grid_layout.py b/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
index a3b44b4c6..a7c3e8dce 100644
--- a/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
+++ b/src/tagstudio/qt/views/layouts/thumb_grid_layout.py
@@ -198,7 +198,7 @@ def setGeometry(self, arg__1: QRect) -> None:
return
per_row, width_offset, height_offset = self._size(rect.right())
- view_height = self.parentWidget().parentWidget().height()
+ view_height = self.parentWidget().parentWidget().height() # pyright: ignore[reportOptionalMemberAccess]
offset = self.scroll_area.verticalScrollBar().value()
if self._scroll_to is not None:
try:
diff --git a/tests/qt/test_build_tag_panel.py b/tests/qt/test_build_tag_panel.py
index 825b1682c..3b79815e0 100644
--- a/tests/qt/test_build_tag_panel.py
+++ b/tests/qt/test_build_tag_panel.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
# pyright: reportPrivateUsage = false
+# pyright: reportOptionalMemberAccess=false
from collections.abc import Callable
from typing import cast
From fc125614a2372aa1d74e0c435708a6edbed8f633 Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Wed, 19 Aug 2026 12:11:16 -0700
Subject: [PATCH 7/7] fix(ui): allow valid None value for cancelButtonText to
hide button
---
src/tagstudio/qt/controllers/progress_bar.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/tagstudio/qt/controllers/progress_bar.py b/src/tagstudio/qt/controllers/progress_bar.py
index dd2618f76..59026b8b8 100644
--- a/src/tagstudio/qt/controllers/progress_bar.py
+++ b/src/tagstudio/qt/controllers/progress_bar.py
@@ -28,7 +28,7 @@ def __init__(
self.root = QVBoxLayout(self)
self.pb = QProgressDialog(
label_text,
- cancel_button_text or "",
+ cancel_button_text, # pyright: ignore[reportArgumentType]
minimum,
maximum,
)