From f5a30489d7b711fea2cf20cc22090b1bde79324e Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:22:25 -0700 Subject: [PATCH] build: bump python version to 3.14 (#1485) * build: bump python to 3.14 * ci: bump python to 3.14, update docs * build: remove version condition for audioop-lts * chore: formatting pass * fix: disallow 3.15.0 * build: bump pyside version to 6.11.2 * fix(ui): allow valid None value for cancelButtonText to hide button --- .github/actions/setup-python/action.yml | 2 +- docs/developing.md | 6 +-- docs/install.md | 2 +- pyproject.toml | 8 ++-- 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/previews/renderers/audio.py | 2 +- src/tagstudio/qt/app_settings.py | 2 +- .../qt/controllers/autofill_line_edit.py | 2 +- .../qt/controllers/field_suggest_box.py | 4 +- .../field_template_search_panel.py | 4 +- .../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/progress_bar.py | 8 ++-- src/tagstudio/qt/controllers/search_panel.py | 4 +- src/tagstudio/qt/controllers/suggest_box.py | 11 ++++-- src/tagstudio/qt/controllers/tag_box.py | 2 +- .../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 | 6 +-- 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 | 13 ++++--- src/tagstudio/qt/mixed/file_attributes.py | 8 ++-- 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 | 7 ++-- 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 | 39 ++++++++----------- .../qt/mixed/mirror_entries_modal.py | 2 +- src/tagstudio/qt/mixed/paged_panel.py | 5 ++- src/tagstudio/qt/mixed/pagination.py | 1 + .../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 | 6 ++- 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 +- src/tagstudio/qt/views/layouts/flow_layout.py | 6 ++- .../qt/views/layouts/thumb_grid_layout.py | 4 +- .../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 +- tests/qt/test_build_tag_panel.py | 1 + 63 files changed, 162 insertions(+), 146 deletions(-) diff --git a/.github/actions/setup-python/action.yml b/.github/actions/setup-python/action.yml index 82171987..8e25e2b0 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 71726351..cd539337 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 3abb853d..144ed8d4 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). diff --git a/pyproject.toml b/pyproject.toml index f5a29a70..a0ec9bb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,9 @@ 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'", + "audioop-lts~=0.2.2", "chardet~=5.2", "ffmpeg-python~=0.2", "humanfriendly==10.*", @@ -22,12 +22,12 @@ 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", "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/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 7e16199c..0c3c55c4 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 @@ class BrowsingState: 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 @@ class BrowsingState: 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 c7bd0731..4daf42d5 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 @@ class Tag(Base): 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 @@ class Tag(Base): 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 e3370e2a..46f09f47 100644 --- a/src/tagstudio/core/library/json/library.py +++ b/src/tagstudio/core/library/json/library.py @@ -861,7 +861,7 @@ class Library: 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 c8268ea4..25ebfcb6 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 @@ class ConstraintType(Enum): class AST: - parent: "AST | None" = None + parent: AST | None = None @override def __str__(self): @@ -65,9 +65,9 @@ class ORList(AST): 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 fc6e6685..45b04a09 100644 --- a/src/tagstudio/core/query_lang/tokenizer.py +++ b/src/tagstudio/core/query_lang/tokenizer.py @@ -39,11 +39,11 @@ class Token: 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 2f0f544d..3e8861ce 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 7c9c0195..23ca1d6c 100644 --- a/src/tagstudio/i18n/translations.py +++ b/src/tagstudio/i18n/translations.py @@ -86,7 +86,7 @@ class Translator: 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 d2918e51..418d1e38 100644 --- a/src/tagstudio/previews/renderers/archive.py +++ b/src/tagstudio/previews/renderers/archive.py @@ -52,7 +52,7 @@ class TarFile: 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/previews/renderers/audio.py b/src/tagstudio/previews/renderers/audio.py index 262e484c..6b981a7e 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/app_settings.py b/src/tagstudio/qt/app_settings.py index 5817e4a6..450fa788 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/autofill_line_edit.py b/src/tagstudio/qt/controllers/autofill_line_edit.py index dfc5a451..1e481258 100644 --- a/src/tagstudio/qt/controllers/autofill_line_edit.py +++ b/src/tagstudio/qt/controllers/autofill_line_edit.py @@ -77,4 +77,4 @@ class AutofillLineEdit(QLineEdit): # 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 199de7f3..e1d3efcc 100644 --- a/src/tagstudio/qt/controllers/field_suggest_box.py +++ b/src/tagstudio/qt/controllers/field_suggest_box.py @@ -11,6 +11,7 @@ from PySide6.QtWidgets import QWidget 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 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): 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 d131b3f4..f1b4c919 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 PySide6.QtWidgets import QMessageBox, QWidget 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 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): 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/fix_ignored_modal.py b/src/tagstudio/qt/controllers/fix_ignored_modal.py index 341469e0..4704a7f8 100644 --- a/src/tagstudio/qt/controllers/fix_ignored_modal.py +++ b/src/tagstudio/qt/controllers/fix_ignored_modal.py @@ -24,7 +24,7 @@ logger = structlog.get_logger(__name__) # 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 b20871be..4e98c546 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 9dddd034..1f18cfac 100644 --- a/src/tagstudio/qt/controllers/library_info_window.py +++ b/src/tagstudio/qt/controllers/library_info_window.py @@ -33,7 +33,7 @@ logger = structlog.get_logger(__name__) # 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 6bf6b9fb..98eeb4ba 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 @@ class MainWindow(QMainWindow): # 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 @@ class MainWindow(QMainWindow): 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 @@ class MainWindow(QMainWindow): 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 @@ class MainWindow(QMainWindow): 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 d7973379..db73ea17 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 @@ if typing.TYPE_CHECKING: 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 d20723a2..8e99d2df 100644 --- a/src/tagstudio/qt/controllers/preview_thumb.py +++ b/src/tagstudio/qt/controllers/preview_thumb.py @@ -33,7 +33,7 @@ Image.MAX_IMAGE_PIXELS = None # 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/progress_bar.py b/src/tagstudio/qt/controllers/progress_bar.py index 98b5fc7b..59026b8b 100644 --- a/src/tagstudio/qt/controllers/progress_bar.py +++ b/src/tagstudio/qt/controllers/progress_bar.py @@ -27,10 +27,10 @@ class ProgressWidget(QWidget): 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, # pyright: ignore[reportArgumentType] + 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 9518ac1e..96f92257 100644 --- a/src/tagstudio/qt/controllers/search_panel.py +++ b/src/tagstudio/qt/controllers/search_panel.py @@ -78,7 +78,7 @@ class SearchPanel[T](ModalContent): 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( @@ -166,7 +166,7 @@ class SearchPanel[T](ModalContent): 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 b8c33160..9627aa04 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -11,6 +11,7 @@ from PySide6.QtGui import QAction, QPixmap, QShowEvent, Qt 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 @@ class SuggestBox[T](QWidget): 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 @@ class SuggestBox[T](QWidget): # 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 @@ class SuggestBox[T](QWidget): # 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 @@ class SuggestBox[T](QWidget): 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_box.py b/src/tagstudio/qt/controllers/tag_box.py index a6bdcbd8..f174522e 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/controllers/tag_search_panel.py b/src/tagstudio/qt/controllers/tag_search_panel.py index 1f469fa8..7771747b 100644 --- a/src/tagstudio/qt/controllers/tag_search_panel.py +++ b/src/tagstudio/qt/controllers/tag_search_panel.py @@ -12,6 +12,7 @@ from PySide6.QtWidgets import QMessageBox, QWidget 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 @@ class TagSearchPanel(SearchPanel[Tag]): 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 d7735ba7..04789cd7 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box.py @@ -12,6 +12,7 @@ from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget 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 @@ class TagSuggestBox(SuggestBox[Tag]): 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 8c041f3b..92875845 100644 --- a/src/tagstudio/qt/controllers/underlined_widget.py +++ b/src/tagstudio/qt/controllers/underlined_widget.py @@ -6,6 +6,7 @@ from typing import override 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 @@ class UnderlinedWidget(QWidget): @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 3ce3e111..84472093 100644 --- a/src/tagstudio/qt/mixed/build_tag.py +++ b/src/tagstudio/qt/mixed/build_tag.py @@ -359,7 +359,7 @@ class BuildTagPanel(ModalContent): 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 @@ class BuildTagPanel(ModalContent): 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 39a0577d..03e0e64e 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 @@ -60,7 +60,7 @@ class ColorBoxWidget(FieldWidget): 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/datetime_picker.py b/src/tagstudio/qt/mixed/datetime_picker.py index cca8e91f..ef5bb4cf 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 df2da028..85909758 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 2997731c..daf57109 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 @@ class FieldContainers(QWidget): 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 914ab7f6..4b699678 100644 --- a/src/tagstudio/qt/mixed/field_widget.py +++ b/src/tagstudio/qt/mixed/field_widget.py @@ -12,6 +12,7 @@ from PySide6.QtCore import QEvent, QSize, Qt 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 @@ -136,17 +137,19 @@ class FieldContainer(QWidget): if callback: 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() + def set_inner_widget(self, widget: FieldWidget) -> None: + 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 9409bb66..43071a39 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) @@ -130,7 +130,7 @@ class FileAttributes(QWidget): 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 @@ class FileAttributes(QWidget): 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 @@ class FileAttributes(QWidget): 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/fix_dupe_files.py b/src/tagstudio/qt/mixed/fix_dupe_files.py index cc275c44..cfd5f48f 100644 --- a/src/tagstudio/qt/mixed/fix_dupe_files.py +++ b/src/tagstudio/qt/mixed/fix_dupe_files.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: # 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 56237134..39cad7c7 100644 --- a/src/tagstudio/qt/mixed/fix_unlinked.py +++ b/src/tagstudio/qt/mixed/fix_unlinked.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: # 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 45908b0b..97760caa 100644 --- a/src/tagstudio/qt/mixed/folders_to_tags.py +++ b/src/tagstudio/qt/mixed/folders_to_tags.py @@ -1,11 +1,13 @@ # SPDX-FileCopyrightText: (c) TagStudio Contributors # SPDX-License-Identifier: GPL-3.0-only +# pyright: reportOptionalMemberAccess=false import math 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 +21,6 @@ from PySide6.QtWidgets import ( 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 +40,7 @@ logger = structlog.get_logger(__name__) @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 +166,7 @@ def generate_preview_data(library: Library) -> BranchData: 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 6ebf8707..b89a2f20 100644 --- a/src/tagstudio/qt/mixed/item_thumb.py +++ b/src/tagstudio/qt/mixed/item_thumb.py @@ -96,7 +96,7 @@ class ItemThumb(FlowWidget): self, mode: ItemType | None, library: Library, - driver: "QtDriver", + driver: QtDriver, thumb_size: tuple[int, int], show_filename_label: bool = False, ): @@ -440,7 +440,7 @@ class ItemThumb(FlowWidget): 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 a738ac0f..30d042a5 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 7de45359..a8029a31 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 65457e10..37aab223 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 PySide6.QtWidgets import ( ) from sqlalchemy import select from sqlalchemy.orm import Session -from typing_extensions import deprecated from tagstudio.core.constants import ( IGNORE_NAME, @@ -98,8 +98,8 @@ class JsonMigrationModal(QObject): 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 @@ class JsonMigrationModal(QObject): 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 @@ class JsonMigrationModal(QObject): 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 @@ class JsonMigrationModal(QObject): 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/mirror_entries_modal.py b/src/tagstudio/qt/mixed/mirror_entries_modal.py index 66b964ce..3ec9ea61 100644 --- a/src/tagstudio/qt/mixed/mirror_entries_modal.py +++ b/src/tagstudio/qt/mixed/mirror_entries_modal.py @@ -22,7 +22,7 @@ if typing.TYPE_CHECKING: 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/paged_panel.py b/src/tagstudio/qt/mixed/paged_panel.py index 8c55cb62..4d356463 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 import QtCore, QtGui 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 @@ class PagedPanel(QWidget): # 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 b10be79d..78cf7316 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/remove_ignored_modal.py b/src/tagstudio/qt/mixed/remove_ignored_modal.py index 0870770e..3787ba75 100644 --- a/src/tagstudio/qt/mixed/remove_ignored_modal.py +++ b/src/tagstudio/qt/mixed/remove_ignored_modal.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: 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 ff6bbcf1..d34fa02b 100644 --- a/src/tagstudio/qt/mixed/remove_unlinked_modal.py +++ b/src/tagstudio/qt/mixed/remove_unlinked_modal.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: 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 3a104398..393616b0 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -41,7 +41,7 @@ logger = structlog.get_logger(__name__) # 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 @@ class SettingsPanel(ModalContent): "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 @@ class SettingsPanel(ModalContent): ) @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 bcbea886..abb09100 100644 --- a/src/tagstudio/qt/mixed/tag_color_label.py +++ b/src/tagstudio/qt/mixed/tag_color_label.py @@ -41,7 +41,7 @@ class TagColorLabel(QWidget): 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 12df1b4f..5723616a 100644 --- a/src/tagstudio/qt/mixed/tag_color_manager.py +++ b/src/tagstudio/qt/mixed/tag_color_manager.py @@ -23,6 +23,7 @@ from PySide6.QtWidgets import ( 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 @@ -42,7 +43,7 @@ class TagColorManager(QWidget): def __init__( self, - driver: "QtDriver", + driver: QtDriver, ): super().__init__() self.driver = driver @@ -166,7 +167,8 @@ class TagColorManager(QWidget): 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/mixed/tag_color_preview.py b/src/tagstudio/qt/mixed/tag_color_preview.py index 098569e2..7ad40e65 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 c65ab366..05b9ca28 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 d769a4ce..7eb28ddf 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 a164c4a2..1e9d9224 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 @@ if TYPE_CHECKING: # 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 a0732ac1..3195d0e2 100644 --- a/src/tagstudio/qt/views/inspector_view.py +++ b/src/tagstudio/qt/views/inspector_view.py @@ -28,7 +28,7 @@ logger = structlog.get_logger(__name__) 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/flow_layout.py b/src/tagstudio/qt/views/layouts/flow_layout.py index af14a7b2..646998d4 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 @@ class FlowLayout(QLayout): 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 12c2e68f..a7c3e8dc 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 @@ -198,7 +198,7 @@ class ThumbGridLayout(QLayout): 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/src/tagstudio/qt/views/library_info_window_view.py b/src/tagstudio/qt/views/library_info_window_view.py index 46cecd6c..623066ed 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 @@ if TYPE_CHECKING: # 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 f3ca286a..5fcb922f 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 0d3ddde1..f66829a2 100644 --- a/src/tagstudio/qt/views/tag_box_view.py +++ b/src/tagstudio/qt/views/tag_box_view.py @@ -23,7 +23,7 @@ logger = structlog.get_logger(__name__) 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 diff --git a/tests/qt/test_build_tag_panel.py b/tests/qt/test_build_tag_panel.py index 825b1682..3b79815e 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