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). diff --git a/pyproject.toml b/pyproject.toml index f5a29a706..a0ec9bb53 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 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/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/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/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/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/progress_bar.py b/src/tagstudio/qt/controllers/progress_bar.py index 98b5fc7b2..59026b8b8 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, # 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 9518ac1e2..96f922574 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( @@ -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_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/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 39a0577d7..03e0e64ed 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 @@ 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/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..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 @@ -136,17 +137,19 @@ 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: - 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 9409bb66c..43071a398 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 @@ 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/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..97760caa0 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 @@ 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 @@ @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 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..37aab223b 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, @@ -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/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/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/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..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 @@ -42,7 +43,7 @@ class TagColorManager(QWidget): def __init__( self, - driver: "QtDriver", + driver: QtDriver, ): super().__init__() self.driver = driver @@ -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/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/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 12c2e68fa..a7c3e8dce 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 @@ 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/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 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