Compare commits

...

8 Commits

Author SHA1 Message Date
Hosted Weblate 01ca419a67 Translated using Weblate (Viossa)
Currently translated at 88.2% (322 of 365 strings)

Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/qpv/
Translation: TagStudio/Strings
2026-05-15 01:11:46 +02:00
Hosted Weblate 24ee7c319f Translated using Weblate (Japanese)
Currently translated at 98.6% (360 of 365 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Strings
2026-05-15 01:11:46 +02:00
Travis Abendshien 38da7bb3a9 fix: drop type_key column from text_fields and datetime_fields tables (#1370)
* fix: drop type_key column from text_fields and datetime_fields tables

* fix: commit empty DB_VERSION 200 library

* fix: use recommended renaming order for field tables
2026-05-14 13:38:52 -07:00
dependabot[bot] ab3394ad40 chore(deps-dev): bump pytest from 8.3.5 to 9.0.3 (#1372)
* chore(deps-dev): bump pytest from 8.3.5 to 9.0.3

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.5...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: bump syrupy to 5.1.0

Version prior to 5.0.0 are incompatible with pytest 9.x.x

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jann Stute <jann.stute@protonmail.com>
2026-05-14 12:25:40 -07:00
Travis Abendshien 41f9749814 refactor(ci): remove mypy (#1371)
* refactor(ci): remove mypy

* fix(ci): restore 'type: ignore' comment for st_birthtime property
2026-05-14 14:24:26 +02:00
Jann Stute 2ab8203967 ci: add pyright workflow (#1232)
* ci: initial version of pyright workflow

* ci: remove invalid pyright settings

* debug: return ALL results from pyright

* ci: remove unnecessary reviewdog install

* Revert "debug: return ALL results from pyright"

This reverts commit 471e39d29f.

* ci: choose reporter based on event type

* ci: choose correct reporter for non-pr event

* ci: small cleanup

* ci: also install pytest deps

* doc: add license info for new workflow

* chore: bump pyright patch version

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* fix: add back settings for basedpyright

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2026-05-14 01:45:52 -07:00
Travis Abendshien 3846b65758 fix(ci): address remaining pyright errors (#1368)
* fix(ci): address remaining pyright errors

* fix: implement review feedback
2026-05-13 21:28:41 +02:00
Jann Stute a7b6636a0b fix: pyright errors in ts_qt.py (#1237) 2026-05-13 02:28:24 -04:00
35 changed files with 293 additions and 198 deletions
@@ -1,23 +1,18 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: MyPy
name: Pyright
on: [push, pull_request]
jobs:
mypy:
name: Run MyPy
pyright:
name: Run Pyright
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Setup Python
uses: actions/setup-python@v5
with:
@@ -27,10 +22,10 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[mypy]
uv pip install --system .[pyright,pytest]
- name: Execute MyPy
uses: tsuyoshicho/action-mypy@v4
- name: Execute Pyright
uses: jordemort/action-pyright@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
fail_on_error: true
reporter: ${{ github.event_name == 'pull_request' && 'github-pr-review' || 'github-check' }}
+3 -3
View File
@@ -2,9 +2,9 @@
repos:
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
- id: pyright
name: pyright
entry: pyright
language: system
types_or: [python, pyi]
require_serial: true
+23 -12
View File
@@ -27,7 +27,7 @@ Thank you so much for showing interest in contributing to TagStudio! Here are a
- I've read the [FAQ](https://github.com/TagStudioDev/TagStudio/blob/main/README.md#faq)
- I've checked the project's [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls)
- **I've created a new issue for my feature/fix _before_ starting work on it**, or have at least notified others in the relevant existing issue(s) of my intention to work on it
- I've set up my development environment including Ruff, Mypy, and PyTest
- I've set up my development environment including Ruff, Pyright, and Pytest
- I've read the CONTRIBUTING.md/Contributing page on the documentation site as well as the and/or [Style Guide](style.md)
- **_I mean it, I've found or created an issue for my feature/fix!_**
@@ -75,7 +75,11 @@ When pushing your code, several automated workflows will check it against predef
### [Ruff](https://github.com/astral-sh/ruff)
A Python linter and code formatter. Ruff uses the `pyproject.toml` as its config file and runs whenever code is pushed or pulled into the project.
Ruff is a Python linter and code formatter that helps enforce a consistent formatting style across our codebase.
Ruff is installed alongside the `pip install -e ".[dev]"` command, but is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
Our Ruff workflow runs whenever a pull request is opened or commits are pushed. Note that this workflow **does NOT** format code, but just serves to report any issues that have not been addressed by that point.
#### Running Locally
@@ -87,24 +91,31 @@ Inside the root repository directory:
Ruff should automatically discover the configuration options inside the [pyproject.toml](https://github.com/TagStudioDev/TagStudio/blob/main/pyproject.toml) file. For more information, see the [ruff configuration discovery docs](https://docs.astral.sh/ruff/configuration/#config-file-discovery).
Ruff is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
### [Pyright](https://github.com/microsoft/pyright)
### [Mypy](https://github.com/python/mypy)
Pyright is a static type checker for Python that helps enforce type strictness and prevent easy-to-miss errors across our codebase.
Mypy is a static type checker for Python. It sure has a lot to say sometimes, but we recommend you take its advice when possible. Mypy also uses the `pyproject.toml` as its config file and runs whenever code is pushed or pulled into the project.
Pyright is installed alongside the `pip install -e ".[dev]"` command, but is also available as VS Code extensions (see the [Pyright](https://marketplace.visualstudio.com/items?itemName=ms-pyright.pyright), [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance), and [basedpyright](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) extensions), a PyCharm [setting](https://www.jetbrains.com/help/pycharm/lsp-tools.html#pyright), or in the form of forks such as [basedpyright](https://docs.basedpyright.com/latest/).
Our Pyright workflow runs whenever a pull request is opened or commits are pushed. Note that this **does NOT** format code or fix issues, but just serves to report any issues that have not been addressed by that point.
#### Running Locally
- **(First time only)** Run the following:
- `mkdir -p .mypy_cache`
- `mypy --install-types --non-interactive`
- You can now check code by running `mypy --config-file pyproject.toml .` in the repository root. _(Don't forget the "." at the end!)_
- Run `pyright` to check code
Mypy is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=matangover.mypy), PyCharm [plugin](https://plugins.jetbrains.com/plugin/11086-mypy), and [more](https://plugins.jetbrains.com/plugin/11086-mypy).
Pyright/basedpyright should automatically discover the configuration options inside the [pyproject.toml](https://github.com/TagStudioDev/TagStudio/blob/main/pyproject.toml) file.
### PyTest
### [Pytest](https://github.com/pytest-dev/pytest)
- Run all tests by running `pytest tests/` in the repository root.
Pytest is our testing suite and runs all our Python code against the tests in the [`tests/`](https://github.com/TagStudioDev/TagStudio/tree/main/tests) directory.
Pytest is installed alongside the `pip install -e ".[dev]"` command.
Our Pytest workflow runs whenever a pull request is opened or commits are pushed.
#### Running Locally
- Run all tests by running `pytest tests/` in the repository root
## Code Style
+1 -1
View File
@@ -146,7 +146,7 @@ The entry point for TagStudio is `src/tagstudio/main.py`. You can target this fi
### pre-commit
There is a [pre-commit](https://pre-commit.com/) configuration that will run through some checks before code is committed. Namely, mypy and the Ruff linter and formatter will check your code, catching those nits right away.
There is a [pre-commit](https://pre-commit.com/) configuration that will run through some checks before code is committed. Namely Pyright and Ruff will check your code, catching those nits right away.
Once you have pre-commit installed, just run:
+12 -3
View File
@@ -148,9 +148,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 200
| Used From | Format | Location |
| --------- | ------ | ----------------------------------------------- |
| TBD | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Used From | Format | Location |
| ---------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [c15e2b5](https://github.com/TagStudioDev/TagStudio/commit/c15e2b56eedd0a3c13391fa43571b8f8f7c7a91f) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds `text_field_templates` and `date_field_templates` tables.
- Drops `boolean_fields` and `value_type` tables.
@@ -162,3 +162,12 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
- Values are set to `TRUE` if the field row was previously a "TEXT_BOX" type.
- Repairs existing "Description" fields inside the `text_fields` table to have their `is_multiline` column set to `TRUE` _(Previously done in [Version 7](#version-7))_.
- Repairs existing "Comments" fields inside the `text_fields` table to have their `is_multiline` column set to `TRUE`.
#### Version 201
| Used From | Format | Location |
| --------- | ------ | ----------------------------------------------- |
| TBD | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Drops `type_key` columns from `text_fields` and `datetime_fields` tables.
- Enforces column positions for `text_fields` and `datetime_fields` tables.
+8 -30
View File
@@ -42,17 +42,17 @@ dependencies = [
]
[project.optional-dependencies]
dev = ["tagstudio[mkdocs,mypy,pre-commit,pyinstaller,pytest,ruff]"]
dev = ["tagstudio[mkdocs,pyright,pre-commit,pyinstaller,pytest,ruff]"]
mkdocs = ["mkdocs-material[imaging]>=9.6.14", "mkdocs-redirects~=1.2"]
mypy = ["mypy==1.15.0", "mypy-extensions==1.*", "types-ujson~=5.10"]
pyright = ["pyright~=1.1.409"]
pre-commit = ["pre-commit~=4.2"]
pyinstaller = ["Pyinstaller~=6.13"]
pytest = [
"pytest==8.3.5",
"pytest==9.0.3",
"pytest-cov==6.1.1",
"pytest-mock==3.15.1",
"pytest-qt==4.4.0",
"syrupy==4.9.1",
"syrupy==5.1.0",
]
ruff = ["ruff==0.11.8"]
@@ -62,31 +62,6 @@ tagstudio = "tagstudio.main:main"
[tool.hatch.build.targets.wheel]
packages = ["src/tagstudio"]
[tool.mypy]
mypy_path = ["src/tagstudio"]
disable_error_code = [
"annotation-unchecked",
"func-returns-value",
"import-untyped",
]
explicit_package_bases = true
ignore_missing_imports = true
implicit_optional = true
strict_optional = false
warn_unused_ignores = true
exclude = ["build", "dist"]
[[tool.mypy.overrides]]
module = "tagstudio.qt.main_window"
ignore_errors = true
[[tool.mypy.overrides]]
module = "tagstudio.qt.ui.home_ui"
ignore_errors = true
[[tool.mypy.overrides]]
module = "tagstudio.core.ts_core"
ignore_errors = true
[tool.pytest.ini_options]
#addopts = "-m 'not qt'"
@@ -99,17 +74,20 @@ ignore = [
"src/tagstudio/qt/previews/vendored/pydub/",
]
include = ["src/tagstudio", "tests"]
# reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
reportAny = false
reportIgnoreCommentWithoutRule = false
reportImplicitStringConcatenation = false
reportImportCycles = false
reportMissingTypeArgument = false
reportMissingTypeStubs = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownLambdaType = false
reportUnknownMemberType = false
reportUnusedCallResult = false
reportUnannotatedClassAttribute = false
reportUninitializedInstanceVariable = false
[tool.ruff]
exclude = ["home_ui.py", "resources.py", "resources_rc.py"]
@@ -9,7 +9,7 @@ JSON_FILENAME: str = "ts_library.json"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 200
DB_VERSION: int = 201
TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
+6 -6
View File
@@ -20,19 +20,19 @@ class BaseField(Base):
@declared_attr
def id(self) -> Mapped[int]:
return mapped_column(primary_key=True, autoincrement=True)
return mapped_column(primary_key=True, autoincrement=True, sort_order=1)
@declared_attr
def name(self) -> Mapped[str]:
return mapped_column(nullable=False, default="")
return mapped_column(nullable=False, default="", sort_order=2)
@declared_attr
def entry_id(self) -> Mapped[int]:
return mapped_column(ForeignKey("entries.id"))
return mapped_column(ForeignKey("entries.id"), sort_order=3)
@declared_attr
def entry(self) -> Mapped[Entry]:
return relationship(foreign_keys=[self.entry_id]) # type: ignore # pyright: ignore[reportArgumentType]
return relationship(foreign_keys=[self.entry_id]) # pyright: ignore[reportArgumentType]
@property
def class_name(self) -> str:
@@ -47,7 +47,7 @@ class BaseField(Base):
class TextField(BaseField):
__tablename__ = "text_fields"
value: Mapped[str | None]
value: Mapped[str | None] = mapped_column(sort_order=4)
is_multiline: Mapped[bool] = mapped_column(nullable=False, default=False)
@override
@@ -75,7 +75,7 @@ class TextField(BaseField):
class DatetimeField(BaseField):
__tablename__ = "datetime_fields"
value: Mapped[str | None]
value: Mapped[str | None] = mapped_column(sort_order=4)
@override
def __eq__(self, other: object) -> bool:
+60 -7
View File
@@ -419,6 +419,7 @@ class Library:
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
poolclass = None if storage_path == ":memory:" else NullPool
loaded_db_version: int = 0
initial_db_version: int = DB_VERSION
logger.info(
"[Library] Opening SQLite Library",
@@ -430,6 +431,7 @@ class Library:
# Don't check DB version when creating new library
if not is_new:
loaded_db_version = self.get_version(DB_VERSION_CURRENT_KEY)
initial_db_version = self.get_version(DB_VERSION_INITIAL_KEY)
# ======================== Library Database Version Checking =======================
# DB_VERSION 6 is the first supported SQLite DB version.
@@ -452,7 +454,7 @@ class Library:
),
)
logger.info(f"[Library] DB_VERSION: {loaded_db_version}")
logger.info(f"[Library] Library DB version: {loaded_db_version}")
make_tables(self.engine)
if is_new:
@@ -571,6 +573,9 @@ class Library:
self.__apply_db104_migrations(session, library_dir)
if loaded_db_version < 200:
self.__apply_db200_migrations(session)
# changes: field tables
if initial_db_version < 200 and loaded_db_version < 201:
self.__apply_db201_migrations(session)
session.execute(
text("CREATE INDEX IF NOT EXISTS idx_tags_name_shorthand ON tags (name, shorthand)")
@@ -588,6 +593,7 @@ class Library:
# Update DB_VERSION
if loaded_db_version < DB_VERSION:
logger.info(f"[Library] Library migrated to DB version {DB_VERSION}")
self.set_version(DB_VERSION_CURRENT_KEY, DB_VERSION)
# everything is fine, set the library path
@@ -808,10 +814,6 @@ class Library:
session.execute(text("UPDATE datetime_fields SET name = type_key"))
session.flush()
# TODO: Remove `type_key` columns from text_fields and datetime_fields tables.
# See issue with dropping columns foreign keys in SQLite:
# https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes
# Change `name` values to title case
logger.info("[Library][Migration][200] Normalizing TextField names...")
for text_field in session.execute(select(TextField)).scalars():
@@ -863,6 +865,57 @@ class Library:
session.commit()
def __apply_db201_migrations(self, session: Session):
"""Migrate DB to DB_VERSION 201."""
with session:
create_text_fields_table = text("""
CREATE TABLE text_fields_new (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL,
entry_id INTEGER NOT NULL,
value VARCHAR,
is_multiline BOOLEAN NOT NULL,
FOREIGN KEY(entry_id) REFERENCES entries (id)
)
""")
create_datetime_fields_table = text("""
CREATE TABLE datetime_fields_new (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL,
entry_id INTEGER NOT NULL,
value VARCHAR,
FOREIGN KEY(entry_id) REFERENCES entries (id)
)
""")
logger.info("[Library][Migration][201] Dropping type_key from text_fields table...")
session.execute(create_text_fields_table)
session.flush()
session.execute(
text("""
INSERT INTO text_fields_new (id, name, entry_id, value, is_multiline)
SELECT id, name, entry_id, value, is_multiline
FROM text_fields
""")
)
session.execute(text("DROP TABLE text_fields"))
session.execute(text("ALTER TABLE text_fields_new RENAME TO text_fields"))
logger.info("[Library][Migration][201] Dropping type_key from datetime_fields table...")
session.execute(create_datetime_fields_table)
session.flush()
session.execute(
text("""
INSERT INTO datetime_fields_new (id, name, entry_id, value)
SELECT id, name, entry_id, value
FROM datetime_fields
""")
)
session.execute(text("DROP TABLE datetime_fields"))
session.execute(text("ALTER TABLE datetime_fields_new RENAME TO datetime_fields"))
session.commit()
@property
def field_templates(self) -> Sequence[BaseFieldTemplate]:
with Session(self.engine) as session:
@@ -957,7 +1010,7 @@ class Library:
)
statement = statement.distinct()
entries: ScalarResult[Entry] | list[Entry] = session.execute(statement).scalars()
entries = entries.unique() # type: ignore
entries = entries.unique()
entry_order_dict = {e_id: order for order, e_id in enumerate(entry_ids)}
entries = sorted(entries, key=lambda e: entry_order_dict[e.id])
@@ -1414,7 +1467,7 @@ class Library:
if tag:
tags.append(tag.id)
else:
new = session.add(Tag(name=string)) # type: ignore
new = session.add(Tag(name=string))
if new:
tags.append(new.id)
session.flush()
@@ -47,21 +47,21 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
self.lib = lib
@override
def visit_or_list(self, node: ORList) -> ColumnElement[bool]: # type: ignore
def visit_or_list(self, node: ORList) -> ColumnElement[bool]:
tag_ids, bool_expressions = self.__separate_tags(node.elements, only_single=False)
if len(tag_ids) > 0:
bool_expressions.append(self.__entry_has_any_tags(tag_ids))
return or_(*bool_expressions)
@override
def visit_and_list(self, node: ANDList) -> ColumnElement[bool]: # type: ignore
def visit_and_list(self, node: ANDList) -> ColumnElement[bool]:
tag_ids, bool_expressions = self.__separate_tags(node.terms, only_single=True)
if len(tag_ids) > 0:
bool_expressions.append(self.__entry_has_all_tags(tag_ids))
return and_(*bool_expressions)
@override
def visit_constraint(self, node: Constraint) -> ColumnElement[bool]: # type: ignore
def visit_constraint(self, node: Constraint) -> ColumnElement[bool]:
"""Returns a Boolean Expression that is true, if the Entry satisfies the constraint."""
if len(node.properties) != 0:
raise NotImplementedError("Properties are not implemented yet") # TODO TSQLANG
@@ -113,11 +113,11 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
raise NotImplementedError("This type of constraint is not implemented yet")
@override
def visit_property(self, node: Property) -> ColumnElement[bool]: # type: ignore
def visit_property(self, node: Property) -> ColumnElement[bool]:
raise NotImplementedError("This should never be reached!")
@override
def visit_not(self, node: Not) -> ColumnElement[bool]: # type: ignore
def visit_not(self, node: Not) -> ColumnElement[bool]:
return ~self.visit(node.child)
def __get_tag_ids(self, tag_name: str, include_children: bool = True) -> list[int]:
@@ -88,6 +88,6 @@ class FixIgnoredEntriesModal(FixIgnoredEntriesModalView):
self.driver.library_info_window.update_cleanup()
@override
def showEvent(self, event: QtGui.QShowEvent) -> None: # type: ignore
def showEvent(self, event: QtGui.QShowEvent) -> None:
self.update_ignored_count()
return super().showEvent(event)
@@ -46,6 +46,6 @@ class IgnoreModal(IgnoreModalView):
Ignore.write_ignore_file(self.lib.library_dir, lines)
@override
def showEvent(self, event: QShowEvent) -> None: # type: ignore
def showEvent(self, event: QShowEvent) -> None:
self.__load_file()
return super().showEvent(event)
@@ -162,6 +162,6 @@ class LibraryInfoWindow(LibraryInfoWindowView):
return size
@override
def showEvent(self, event: QtGui.QShowEvent): # type: ignore
def showEvent(self, event: QtGui.QShowEvent):
self.refresh()
return super().showEvent(event)
@@ -4,7 +4,7 @@
import io
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
import cv2
import rawpy
@@ -12,6 +12,10 @@ import structlog
from PIL import Image, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PySide6.QtCore import QSize
from rawpy import (
LibRawFileUnsupportedError, # pyright: ignore[reportPrivateImportUsage]
LibRawIOError, # pyright: ignore[reportPrivateImportUsage]
)
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.media_types import MediaCategories
@@ -50,8 +54,8 @@ class PreviewThumb(PreviewThumbView):
stats.width = image.width
stats.height = image.height
except (
rawpy.LibRawIOError,
rawpy.LibRawFileUnsupportedError,
LibRawIOError,
LibRawFileUnsupportedError,
FileNotFoundError,
):
pass
@@ -144,18 +148,22 @@ class PreviewThumb(PreviewThumbView):
self._display_image(filepath)
return self.__get_image_stats(filepath)
@override
def _open_file_action_callback(self):
open_file(
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
)
@override
def _open_explorer_action_callback(self):
open_file(self.__current_file, file_manager=True)
@override
def _delete_action_callback(self):
if bool(self.__current_file):
self.__driver.delete_files_callback(self.__current_file)
@override
def _button_wrapper_callback(self):
open_file(
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
@@ -34,7 +34,7 @@ class TagBoxWidget(TagBoxWidgetView):
self.__entries = entries
@override
def _on_click(self, tag: Tag) -> None: # type: ignore[misc]
def _on_click(self, tag: Tag) -> None:
match self.__driver.settings.tag_click_action:
case TagClickActionOption.OPEN_EDIT:
self._on_edit(tag)
@@ -58,7 +58,7 @@ class TagBoxWidget(TagBoxWidgetView):
)
@override
def _on_remove(self, tag: Tag) -> None: # type: ignore[misc]
def _on_remove(self, tag: Tag) -> None:
logger.info(
"[TagBoxWidget] remove_tag",
selected=self.__entries,
@@ -70,7 +70,7 @@ class TagBoxWidget(TagBoxWidgetView):
self.on_update.emit()
@override
def _on_edit(self, tag: Tag) -> None: # type: ignore[misc]
def _on_edit(self, tag: Tag) -> None:
build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag)
edit_modal = PanelModal(
@@ -92,7 +92,7 @@ class TagBoxWidget(TagBoxWidgetView):
edit_modal.show()
@override
def _on_search(self, tag: Tag) -> None: # type: ignore[misc]
def _on_search(self, tag: Tag) -> None:
self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}")
self.__driver.update_browsing_state(
BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current)
+2 -2
View File
@@ -7,7 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
def wrap_line(
text: str,
font: ImageFont.ImageFont,
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
width: int = 256,
draw: ImageDraw.ImageDraw | None = None,
) -> int:
@@ -31,7 +31,7 @@ def wrap_line(
def wrap_full_text(
text: str,
font: ImageFont.ImageFont,
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
width: int = 256,
draw: ImageDraw.ImageDraw | None = None,
) -> str:
+1 -1
View File
@@ -138,7 +138,7 @@ class ColorBoxWidget(FieldWidget):
)
self.edit_modal.saved.connect(
lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit()) # type: ignore
lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit())
)
self.edit_modal.show()
@@ -114,6 +114,7 @@ class FileAttributes(QWidget):
if filepath and filepath.is_file():
created: dt
if platform.system() == "Windows" or platform.system() == "Darwin":
# NOTE: Accessing stat().st_birthtime causes linter checks to fail on some systems.
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
else:
created = dt.fromtimestamp(filepath.stat().st_ctime)
+3 -3
View File
@@ -467,12 +467,12 @@ class ItemThumb(FlowWidget):
badge.setHidden(is_hidden)
@override
def enterEvent(self, event: QEnterEvent) -> None: # type: ignore[misc]
def enterEvent(self, event: QEnterEvent) -> None:
self.show_check_badges(show=True)
return super().enterEvent(event)
@override
def leaveEvent(self, event: QEvent) -> None: # type: ignore[misc]
def leaveEvent(self, event: QEvent) -> None:
self.show_check_badges(show=False)
return super().leaveEvent(event)
@@ -502,7 +502,7 @@ class ItemThumb(FlowWidget):
)
@override
def mouseMoveEvent(self, event: QMouseEvent) -> None: # type: ignore[misc]
def mouseMoveEvent(self, event: QMouseEvent) -> None:
if event.buttons() is not Qt.MouseButton.LeftButton:
return
+1 -1
View File
@@ -324,7 +324,7 @@ class MediaPlayer(QGraphicsView):
"""Manage events for the media player."""
if (
arg__2.type() == QEvent.Type.MouseButtonPress
and arg__2.button() == Qt.MouseButton.LeftButton # type: ignore
and arg__2.button() == Qt.MouseButton.LeftButton # pyright: ignore[reportAttributeAccessIssue]
):
if arg__1 == self.play_pause:
self.toggle_play()
+11 -11
View File
@@ -364,7 +364,7 @@ class JsonMigrationModal(QObject):
iterator = FunctionIterator(self.migration_iterator)
iterator.value.connect(
lambda x: (
pb.setLabelText(f"<h4>{x}</h4>"), # type: ignore
pb.setLabelText(f"<h4>{x}</h4>"),
self.update_sql_value_ui(show_msg_box=False)
if x == Translations["json_migration.checking_for_parity"]
else (),
@@ -377,8 +377,8 @@ class JsonMigrationModal(QObject):
r.done.connect(
lambda: (
self.update_sql_value_ui(show_msg_box=not skip_ui),
pb.setMinimum(1), # type: ignore
pb.setValue(1), # type: ignore
pb.setMinimum(1),
pb.setValue(1),
# Enable the finish button
cast(QPushButtonWrapper, self.stack[1].buttons[4]).setDisabled(False),
)
@@ -483,26 +483,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() # type:ignore
label: QLabel = self.old_content_layout.itemAtPosition(self.entries_row, 1).widget() # pyright: ignore[reportAssignmentType]
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() # type:ignore
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # pyright: ignore[reportAssignmentType]
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() # type:ignore
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # type:ignore
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.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() # type:ignore
new_label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # type:ignore
old_warning_icon: QLabel = self.old_content_layout.itemAtPosition(row, 2).widget() # type:ignore
new_warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # type:ignore
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.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)
+2 -4
View File
@@ -218,7 +218,7 @@ class Pagination(QWidget):
if i < index:
if (i != 0) and i >= index - 4:
self.start_buffer_layout.itemAt(i - start_offset).widget().setHidden(False)
self.start_buffer_layout.itemAt(i - start_offset).widget().setText( # type: ignore
self.start_buffer_layout.itemAt(i - start_offset).widget().setText( # pyright: ignore[reportAttributeAccessIssue]
str(i + 1)
)
self._assign_click(
@@ -237,9 +237,7 @@ class Pagination(QWidget):
elif i > index:
if i != page_count - 1 and i <= index + 4:
self.end_buffer_layout.itemAt(i - end_offset).widget().setHidden(False)
self.end_buffer_layout.itemAt(i - end_offset).widget().setText( # type: ignore
str(i + 1)
)
self.end_buffer_layout.itemAt(i - end_offset).widget().setText(str(i + 1)) # pyright: ignore[reportAttributeAccessIssue]
self._assign_click(
cast(
QPushButtonWrapper,
+1 -1
View File
@@ -62,6 +62,6 @@ class ProgressWidget(QWidget):
r = CustomRunnable(lambda: iterator.run())
r.done.connect(
lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks]) # type: ignore
lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks])
)
QThreadPool.globalInstance().start(r)
+3 -3
View File
@@ -59,15 +59,15 @@ class TagDatabasePanel(TagSearchPanel):
return
message_box = QMessageBox(
QMessageBox.Question, # type: ignore
QMessageBox.Question, # pyright: ignore[reportAttributeAccessIssue]
Translations["tag.remove"],
Translations.format("tag.confirm_delete", tag_name=self.lib.tag_display_name(tag)),
QMessageBox.Ok | QMessageBox.Cancel, # type: ignore
QMessageBox.Ok | QMessageBox.Cancel, # pyright: ignore[reportAttributeAccessIssue]
)
result = message_box.exec()
if result != QMessageBox.Ok: # type: ignore
if result != QMessageBox.Ok: # pyright: ignore[reportAttributeAccessIssue]
return
self.lib.remove_tag(tag.id)
+35 -26
View File
@@ -43,7 +43,7 @@ from PIL import (
UnidentifiedImageError,
)
from PIL.Image import DecompressionBombError
from pillow_heif import register_heif_opener
from pillow_heif import register_heif_opener # pyright: ignore[reportUnknownVariableType]
from PySide6.QtCore import (
QBuffer,
QFile,
@@ -58,6 +58,10 @@ from PySide6.QtCore import (
from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
from PySide6.QtSvg import QSvgRenderer
from rawpy import (
LibRawFileUnsupportedError, # pyright: ignore[reportPrivateImportUsage]
LibRawIOError, # pyright: ignore[reportPrivateImportUsage]
)
from tagstudio.core.constants import (
FONT_SAMPLE_SIZES,
@@ -75,9 +79,11 @@ from tagstudio.qt.helpers.gradients import four_corner_gradient
from tagstudio.qt.helpers.image_effects import replace_transparent_pixels
from tagstudio.qt.helpers.text_wrapper import wrap_full_text
from tagstudio.qt.models.palette import UI_COLORS, ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored.blender_renderer import blend_thumb
from tagstudio.qt.previews.vendored.blender_renderer import (
blend_thumb, # pyright: ignore[reportUnknownVariableType]
)
from tagstudio.qt.previews.vendored.pydub.audio_segment import (
_AudioSegment as AudioSegment,
_AudioSegment as AudioSegment, # pyright: ignore[reportPrivateUsage]
)
from tagstudio.qt.resource_manager import ResourceManager
@@ -118,7 +124,7 @@ class _TarFile:
def __init__(self, filepath: Path, mode: Literal["r"]) -> None:
self.tar: tarfile.TarFile
self.filepath = filepath
self.mode = mode
self.mode: Literal["r"] = mode
def namelist(self) -> list[str]:
return self.tar.getnames()
@@ -127,10 +133,10 @@ class _TarFile:
return unwrap(self.tar.extractfile(name)).read()
def __enter__(self) -> "_TarFile":
self.tar = tarfile.open(self.filepath, self.mode).__enter__()
self.tar = tarfile.open(name=self.filepath, mode=self.mode).__enter__()
return self
def __exit__(self, *args) -> None:
def __exit__(self, *args) -> None: # pyright: ignore[reportUnknownParameterType, reportMissingParameterType]
self.tar.__exit__(*args)
@@ -293,7 +299,7 @@ class ThumbRenderer(QObject):
im: Image.Image = Image.new(
mode="L",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="black",
)
draw = ImageDraw.Draw(im)
@@ -324,7 +330,7 @@ class ThumbRenderer(QObject):
# Highlight
im_hl: Image.Image = Image.new(
mode="RGBA",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#00000000",
)
draw = ImageDraw.Draw(im_hl)
@@ -343,7 +349,7 @@ class ThumbRenderer(QObject):
# Shadow
im_sh: Image.Image = Image.new(
mode="RGBA",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#00000000",
)
draw = ImageDraw.Draw(im_sh)
@@ -388,7 +394,7 @@ class ThumbRenderer(QObject):
# Create larger blank image based on smooth_factor
im: Image.Image = Image.new(
"RGBA",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#FF000000",
)
@@ -396,13 +402,13 @@ class ThumbRenderer(QObject):
bg: Image.Image
bg = Image.new(
"RGB",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#000000FF",
)
# Use a background image if provided
if bg_image:
bg_im = Image.Image.resize(bg_image, size=tuple([d * smooth_factor for d in size])) # type: ignore
bg_im = Image.Image.resize(bg_image, size=tuple([d * smooth_factor for d in size])) # pyright: ignore[reportArgumentType]
bg_im = ImageEnhance.Brightness(bg_im).enhance(0.3) # Reduce the brightness
bg.paste(bg_im)
@@ -411,7 +417,7 @@ class ThumbRenderer(QObject):
bg,
(0, 0),
mask=self._get_mask(
tuple([d * smooth_factor for d in size]), # type: ignore
tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
(pixel_ratio * smooth_factor),
),
)
@@ -495,19 +501,19 @@ class ThumbRenderer(QObject):
# Create larger blank image based on smooth_factor
im: Image.Image = Image.new(
"RGBA",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#00000000",
)
bg: Image.Image
# Use a background image if provided
if bg_image:
bg = Image.Image.resize(bg_image, size=tuple([d * smooth_factor for d in size])) # type: ignore
bg = Image.Image.resize(bg_image, size=tuple([d * smooth_factor for d in size])) # pyright: ignore[reportArgumentType]
# Create solid background color
else:
bg = Image.new(
"RGB",
size=tuple([d * smooth_factor for d in size]), # type: ignore
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#000000",
)
# Apply color overlay
@@ -521,7 +527,7 @@ class ThumbRenderer(QObject):
bg,
(0, 0),
mask=self._get_mask(
tuple([d * smooth_factor for d in size]), # type: ignore
tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
(pixel_ratio * smooth_factor),
),
)
@@ -662,17 +668,17 @@ class ThumbRenderer(QObject):
artwork = None
if ext in [".mp3"]:
id3_tags: id3.ID3 = id3.ID3(filepath)
id3_covers: list = id3_tags.getall("APIC")
id3_covers: list = id3_tags.getall("APIC") # pyright: ignore[reportUnknownVariableType]
if id3_covers:
artwork = Image.open(BytesIO(id3_covers[0].data))
elif ext in [".flac"]:
flac_tags: flac.FLAC = flac.FLAC(filepath)
flac_covers: list = flac_tags.pictures
flac_covers: list = flac_tags.pictures # pyright: ignore[reportUnknownVariableType]
if flac_covers:
artwork = Image.open(BytesIO(flac_covers[0].data))
elif ext in [".mp4", ".m4a", ".aac"]:
mp4_tags: mp4.MP4 = mp4.MP4(filepath)
mp4_covers: list | None = mp4_tags.get("covr") # pyright: ignore[reportAssignmentType]
mp4_covers: list | None = mp4_tags.get("covr") # pyright: ignore[reportUnknownVariableType]
if mp4_covers:
artwork = Image.open(BytesIO(mp4_covers[0]))
if artwork:
@@ -1088,7 +1094,7 @@ class ThumbRenderer(QObject):
font = ImageFont.truetype(filepath, size=font_size)
text_wrapped: str = wrap_full_text(
FONT_SAMPLE_TEXT,
font=font, # pyright: ignore[reportArgumentType]
font=font,
width=size,
draw=draw,
)
@@ -1120,8 +1126,8 @@ class ThumbRenderer(QObject):
)
except (
DecompressionBombError,
rawpy.LibRawIOError,
rawpy.LibRawFileUnsupportedError,
LibRawIOError,
LibRawFileUnsupportedError,
) as e:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
return im
@@ -1137,6 +1143,7 @@ class ThumbRenderer(QObject):
try:
# Load the EXR data to an array and rotate the color space from BGRA -> RGBA
raw_array = cv2.imread(str(filepath), cv2.IMREAD_UNCHANGED)
assert raw_array is not None
raw_array[..., :3] = raw_array[..., 2::-1]
# Correct the gamma of the raw array
@@ -1209,7 +1216,7 @@ class ThumbRenderer(QObject):
# Write the image to a buffer as png
buffer: QBuffer = QBuffer()
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
q_image.save(buffer, "PNG") # type: ignore[call-overload]
q_image.save(device=buffer, format="PNG") # pyright: ignore[reportArgumentType]
# Load the image from the buffer
im = Image.new("RGB", (size, size), color="#1e1e1e")
@@ -1258,7 +1265,7 @@ class ThumbRenderer(QObject):
return im
@staticmethod
def _model_stl_thumb(filepath: Path, size: int) -> Image.Image | None:
def _model_stl_thumb(filepath: Path, size: int) -> Image.Image | None: # pyright: ignore[reportUnusedParameter]
"""Render a thumbnail for an STL file.
Args:
@@ -1331,7 +1338,7 @@ class ThumbRenderer(QObject):
buffer: QBuffer = QBuffer()
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
try:
q_image.save(buffer, "PNG") # type: ignore # pyright: ignore
q_image.save(buffer, "PNG") # pyright: ignore
im = Image.open(BytesIO(buffer.buffer().data()))
finally:
buffer.close()
@@ -1614,6 +1621,7 @@ class ThumbRenderer(QObject):
def fetch_cached_image(file_name: Path):
image: Image.Image | None = None
assert self.driver.cache_manager is not None
cached_path = self.driver.cache_manager.get_file_path(file_name)
if cached_path and cached_path.is_file():
@@ -1876,6 +1884,7 @@ class ThumbRenderer(QObject):
image = self._resize_image(image, (adj_size, adj_size))
if save_to_file and savable_media_type and image:
assert self.driver.cache_manager is not None
self.driver.cache_manager.save_image(image, save_to_file, mode="RGBA")
except (
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from pydub
# type: ignore
import array
+12 -13
View File
@@ -8,7 +8,6 @@
"""A Qt driver for TagStudio."""
import contextlib
import ctypes
import math
import os
@@ -194,7 +193,7 @@ class QtDriver(DriverMixin, QObject):
applied_theme: Theme
lib: Library
cache_manager: CacheManager
cache_manager: CacheManager | None
browsing_history: History[BrowsingState]
@@ -351,7 +350,7 @@ class QtDriver(DriverMixin, QObject):
if os.name == "nt":
appid = "cyanvoxel.tagstudio.9"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) # type: ignore[attr-defined,unused-ignore]
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
self.app.setApplicationName("tagstudio")
self.app.setApplicationDisplayName("TagStudio")
@@ -539,7 +538,7 @@ class QtDriver(DriverMixin, QObject):
# TODO: Move this to a settings screen.
self.main_window.menu_bar.clear_thumb_cache_action.triggered.connect(
lambda: self.cache_manager.clear_cache()
lambda: unwrap(self.cache_manager).clear_cache()
)
# endregion
@@ -913,7 +912,7 @@ class QtDriver(DriverMixin, QObject):
origin_id(id): The entry ID associated with the widget making the call.
"""
entry: Entry | None = None
pending: list[tuple[int, Path]] = []
pending: list[tuple[int | None, Path]] = []
deleted_count: int = 0
selected = self.selected
@@ -921,14 +920,13 @@ class QtDriver(DriverMixin, QObject):
if len(selected) <= 1 and origin_path:
origin_id_ = origin_id
if not origin_id_:
with contextlib.suppress(IndexError):
origin_id_ = selected[0]
if origin_id_ is None:
origin_id_ = selected[0] if len(selected) > 0 else None
pending.append((origin_id_, Path(origin_path)))
else:
for item in selected:
entry = self.lib.get_entry(item)
entry = unwrap(self.lib.get_entry(item))
filepath: Path = entry.path
pending.append((item, filepath))
@@ -950,7 +948,8 @@ class QtDriver(DriverMixin, QObject):
self.main_window.status_bar.showMessage(msg)
self.main_window.status_bar.repaint()
self.lib.remove_entries([e_id])
if e_id is not None:
self.lib.remove_entries([e_id])
if delete_file(library_dir / f):
deleted_count += 1
@@ -1101,7 +1100,7 @@ class QtDriver(DriverMixin, QObject):
pw.hide(),
pw.deleteLater(),
# refresh the library only when new items are added
files_count and self.update_browsing_state(), # type: ignore
files_count and self.update_browsing_state(),
)
)
QThreadPool.globalInstance().start(r)
@@ -1226,10 +1225,10 @@ class QtDriver(DriverMixin, QObject):
for field in self.copy_buffer["fields"]:
exists = False
for e in existing_fields:
if field.type_key == e.type_key and field.value == e.value:
if field == e:
exists = True
if not exists:
self.lib.add_field_to_entries(id, field_id=field.type_key, value=field.value)
self.lib.add_field_to_entries(id, field=field)
self.lib.add_tags_to_entries(id, self.copy_buffer["tags"])
if len(self.selected) > 1:
if TAG_ARCHIVED in self.copy_buffer["tags"]:
+1 -1
View File
@@ -5,7 +5,7 @@
from PySide6.QtCore import QObject, QRunnable, Signal
class CustomRunnable(QRunnable, QObject):
class CustomRunnable(QRunnable, QObject): # pyright: ignore[reportUnsafeMultipleInheritance]
done = Signal()
def __init__(self, function) -> None:
+3 -3
View File
@@ -86,7 +86,7 @@ class ThumbButton(QPushButtonWrapper):
)
@override
def paintEvent(self, arg__1: QPaintEvent) -> None: # type: ignore
def paintEvent(self, arg__1: QPaintEvent) -> None:
super().paintEvent(arg__1)
if self.hovered or self.selected:
painter = QPainter()
@@ -127,13 +127,13 @@ class ThumbButton(QPushButtonWrapper):
painter.end()
@override
def enterEvent(self, event: QEnterEvent) -> None: # type: ignore
def enterEvent(self, event: QEnterEvent) -> None:
self.hovered = True
self.repaint()
return super().enterEvent(event)
@override
def leaveEvent(self, event: QEvent) -> None: # type: ignore
def leaveEvent(self, event: QEvent) -> None:
self.hovered = False
self.repaint()
return super().leaveEvent(event)
+1 -1
View File
@@ -59,7 +59,7 @@
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...",
"entries.running.dialog.title": "新しいファイルエントリを追加",
"entries.tags": "タグ",
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルが TagStudio 以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
"entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件のエントリを再リンク中、{fixed_count} 件を正常に再リンクしました",
"entries.unlinked.relink.manual": "手動で再リンク(&M)",
"entries.unlinked.relink.title": "エントリの再リンク",
+70 -36
View File
@@ -1,6 +1,6 @@
{
"about.config_path": "Mlafuplas fu sentakuzma",
"about.description": "TagStudio zeting ke parjat mlafu au riso mit festaretol, hadafvui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena zeting os mlafufal, jamnai mare fu flanka mlafu au perpa tumam fu kompyu.",
"about.description": "TagStudio - apli per parjat mlafu au riso mit festaretol, hadafsui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena apli os mlafufal, nil mare fu flankamlafu, au na nilraz ti kawri hel parjat fu kompyu.",
"about.documentation": "Mahaklarazma",
"about.license": "Brukruuru",
"about.module.found": "Finnajena",
@@ -10,11 +10,11 @@
"app.pre_release": "De-Gvir",
"app.title": "{base_title} - Mlafuhuomi '{library_dir}'",
"color.color_border": "Bruk minusvikti varge per flanka",
"color.confirm_delete": "Du kestetsa varge \"{color_name}\"?",
"color.confirm_delete": "Du kestetsa varge \"{color_name}\" we?",
"color.delete": "Keste festaretol",
"color.import_pack": "Nasii klaani fu varge",
"color.name": "Namae",
"color.namespace.delete.prompt": "Du kestetsa afto vargetumam? Afto zol keste alting ine vargetumam au sebja!",
"color.namespace.delete.prompt": "Du kestetsa afto vargetumam we? Afto zol keste ALTING ine vargetumam au sebja!",
"color.namespace.delete.title": "Keste vargetumam",
"color.new": "Neo varge",
"color.placeholder": "Varge",
@@ -36,47 +36,61 @@
"edit.copy_fields": "Mverm shiruzmafal",
"edit.paste_fields": "Nasii shiru",
"edit.tag_manager": "Jewalt festaretol",
"entries.duplicate.merge": "Visk sama shiruzmakaban",
"entries.duplicate.merge.label": "Visk sama shiruzmakaban ima...",
"entries.duplicate.refresh": "Gotova sama shiruzmakaban gen",
"entries.duplicates.description": "Mverm fu shiruzmakaban implajena na plus ka ein shiruzmakaban ke tsunaga na sama mlafu na shiruzmabaksu. Na visk afto, zol festa festaretol au mlafushiruzma al mverm fu mlafu kara ine ein stuur shiruzmakaban. Hej nai sama \"mverm fu mlafu\", ke mverm fu mlafu fu du, ekso TagStudio.",
"entries.duplicate.merge": "Visk blisnets shiruzmakaban",
"entries.duplicate.merge.label": "Visk blisnets shiruzmakaban ima...",
"entries.duplicate.refresh": "Gotova blisnets shiruzmakaban gen",
"entries.duplicates.description": "Blisnets shiruzmakaban implajena na plus ka ein shiruzmakaban ke tsunaga na sama mlafu na shiruzmabaksu. Na visk afto, zol festa festaretol au mlafushiruzma al mverm fu mlafu kara ine ein stuur shiruzmakaban. Hej nai sama \"mverm fu mlafu\", ke mverm fu mlafu fu du, ekso TagStudio.",
"entries.generic.refresh_alt": "&Gengotova",
"entries.generic.remove.removing": "Keste shiruzmakaban ima",
"entries.generic.remove.removing_count": "Keste {count} shiruzmakaban...",
"entries.ignored.description": "Mlafu shiruzmakaban bli \"hestujena\" li antadan na mlafuhuomi de hesturuuru fu brukdjin (na '.ts_ignore' mlafu) kawaridan per nai eku sore. Hestujena mlafu long mlafuhuomi na snano sit kemuske mlafu na lasazma kesa kawari hesturuuru.",
"entries.ignored.ignored_count": "Hestujena Shiruzmakaban: {count}",
"entries.ignored.remove": "Keste hestujena shiruzmakaban",
"entries.ignored.remove_alt": "Keste hestujena shiruzmakaban (&v)",
"entries.ignored.scanning": "Taskame mlafuhuomi grun hestujena shiruzmakaban...",
"entries.ignored.title": "Fiks hestujena shiruzmakaban",
"entries.mirror": "&Maha melon fu",
"entries.mirror.confirmation": "Du mahatsa melon fu afto {count} shiruzmakaban?",
"entries.mirror.confirmation": "Du mahatsa melon fu afto {count} shiruzmakaban we?",
"entries.mirror.label": "Maha melon fu {idx}/{total} shiruzmakaban ima...",
"entries.mirror.title": "Maha melon fu shiruzmakaban ima",
"entries.mirror.window_title": "Maha melon fu shiruzmakaban",
"entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?",
"entries.remove.plural.confirm": "Du kestetsa afto <b>{count}</b> shiruzmakaban we? Nil mlafu na shiruzmabaksu bli kestejena.",
"entries.remove.singular.confirm": "Du kestetsa afto shiruzmakaban long mlafuhuomi we? Nil mlafu na shiruzmabaksu bli kestejena.",
"entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...",
"entries.running.dialog.title": "Nasii neo shiruzmakaban fu mlafu ima",
"entries.tags": "Festaretol",
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore tsunaganaijena.<br><br>Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.",
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore kntsunagajena.<br><br>Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.",
"entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen",
"entries.unlinked.relink.manual": "&Tsunaga gen mit hant",
"entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen",
"entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...",
"entries.unlinked.remove": "Keste kntsunagajena shiruzmakaban",
"entries.unlinked.remove_alt": "Keste kntsunagajena shiruzmakaban (&v)",
"entries.unlinked.scanning": "Taskame mlafuhuomi ima grun kntsunagajena shiruzmakaban...",
"entries.unlinked.search_and_relink": "&Suha &&Tsunaga gen",
"entries.unlinked.title": "Fiks tsunaganaijena shiruzmakaban",
"entries.unlinked.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}",
"entries.unlinked.title": "Fiks kntsunagajena shiruzmakaban",
"entries.unlinked.unlinked_count": "Kntsunagajena shiruzmakaban: {count}",
"ffmpeg.missing.description": "FFmpeg au/os FFprobe nai finnajena. TagStudio treng FFmpeg per mahase riso.",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Mverm shiruzmafal",
"field.edit": "Kawari shiruzmafal",
"field.paste": "Nasii shiruzmafal",
"file.date_added": "Dag nasiijenadan",
"file.date_created": "Dag mahajenadan",
"file.date_modified": "Dag kawarijenadan",
"field_type.datetime": "Dag-tid",
"field_type.text": "Tekst",
"field_type.unknown": "Fal nai shirujena",
"file.date_added": "Dag antajena",
"file.date_created": "Dag mahajena",
"file.date_modified": "Dag kawarijena",
"file.dimensions": "Stuuratailasku",
"file.duplicates.description": "TagStudio kjoka nasii shiruzma DupeGuru kara per jewalt mverm.",
"file.duplicates.dupeguru.advice": "Za melon mahajena, du deki bruk DupeGuru per keste nai vilena mlafu. Sit, bruk tel fu TagStudio, haisa \"Fiks tsunaganaijena shiruzmakaban\" na tropos sentakutumam per keste tsunaganaijena shiruzmakaban.",
"file.duplicates.description": "TagStudio kjoka nasii shiruzma DupeGuru kara per jewalt blisnets mlafu.",
"file.duplicates.dupeguru.advice": "Za melon mahajena, du deki bruk DupeGuru per keste nai vilena mlafu. Sit, bruk tel fu TagStudio, haisa \"Fiks kntsunagajena shiruzmakaban\" na tropos sentakutumam per keste kntsunagajena shiruzmakaban.",
"file.duplicates.dupeguru.file_extension": "DupeGuru mlafu (*.dupeguru)",
"file.duplicates.dupeguru.load_file": "&Gotova mlafu fu DupeGuru",
"file.duplicates.dupeguru.no_file": "Jamnai sentakujena mlafu fu DupeGuru",
"file.duplicates.dupeguru.open_file": "Auki shiruzma mlafu fu Dupeguru",
"file.duplicates.fix": "Fiks mverm",
"file.duplicates.matches": "Mverm-atai: {count}",
"file.duplicates.matches_uninitialized": "Mverm-atai: N/A",
"file.duplicates.mirror.description": "Maha melon fu shiruzma shiruzmakaban kara na al mverm fu samakaban, afto zol visk al shiruzma, men zolnai keste os mverm shiruzmafal. Afto zolnai keste joku mlafu os shiruzma.",
"file.duplicates.dupeguru.open_file": "Auki sitan mlafu fu Dupeguru",
"file.duplicates.fix": "Fiks blisnets mlafu",
"file.duplicates.matches": "Blisnets-atai: {count}",
"file.duplicates.matches_uninitialized": "blisntes-atai: N/A",
"file.duplicates.mirror.description": "Maha melon fu shiruzma shiruzmakaban kara na al blisnets fu samakaban, afto zol visk al shiruzma, men zolnai keste os mverm shiruzmafal. Afto zolnai keste joku mlafu os shiruzma.",
"file.duplicates.mirror_entries": "&Maha melon fu shiruzmakaban",
"file.duration": "Pitkatai",
"file.not_found": "Mlafu nai finnajena",
@@ -110,33 +124,43 @@
"generic.missing": "Harnai",
"generic.navigation.back": "Suruk",
"generic.navigation.next": "Mirai",
"generic.no": "Nai",
"generic.none": "Nil",
"generic.overwrite": "Vasu",
"generic.overwrite_alt": "&Vasu",
"generic.paste": "Nasii",
"generic.recent_libraries": "Moloda mlafuhuomi",
"generic.remove": "Keste",
"generic.remove_alt": "&Keste",
"generic.rename": "Haisagen",
"generic.rename_alt": "&Haisagen",
"generic.reset": "Gensintua",
"generic.save": "Ufne",
"generic.skip": "Pal",
"generic.skip_alt": "&Pal",
"generic.yes": "Akk",
"home.search": "Suha",
"home.search_entries": "Suha shiruzmakaban",
"home.search_library": "Suha mlafuhuomi",
"home.search_tags": "Suha festaretol",
"home.show_hidden_entries": "Mahase furijena shiruzmakaban",
"home.thumbnail_size": "Stuuratai fu risonen",
"home.thumbnail_size.extra_large": "Plusstuur risonen",
"home.thumbnail_size.large": "Stuur risonen",
"home.thumbnail_size.medium": "Mellan risonen",
"home.thumbnail_size.mini": "Chisai risonen",
"home.thumbnail_size.small": "Chisaidai risonen",
"ignore.open_file": "Mahase \"{ts_ignore}\" mlafu na shiruzmabaksu",
"json_migration.checking_for_parity": "Taskama per chigauzma ima...",
"json_migration.creating_database_tables": "Maha tumam na SQL Database ima...",
"json_migration.description": "<br>Hadji au de se shiruzma fu",
"json_migration.discrepancies_found": "Chiguazma finnajena na mlafuhuomi",
"json_migration.discrepancies_found.description": "Chiguazma finnajena mellan ranja au kjannos. Bitte gense au sentaku per beng na dlabdel os yamete.",
"json_migration.finish_migration": "Owari dlabdel",
"json_migration.heading.aliases": "Andrnamae:",
"json_migration.heading.colors": "Varge:",
"json_migration.heading.differ": "Tchigauzma",
"json_migration.heading.extensions": "Andr ilo:",
"json_migration.heading.match": "Finnajena sama",
"json_migration.heading.names": "Namae:",
"json_migration.heading.parent_tags": "Atama festaretol:",
@@ -156,13 +180,22 @@
"library.field.remove": "Keste shiruzmafal",
"library.missing": "Mlafuplas fu mlafuhuomi nai finnajenadan",
"library.name": "Mlafuhuomi",
"library.refresh.scanning.plural": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
"library.refresh.scanning.singular": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
"library.refresh.scanning_preparing": "Taskama mlafukaban fu neo mlafu ima...\nGotova ima...",
"library.refresh.scanning.plural": "Taskame mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
"library.refresh.scanning.singular": "Taskame mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
"library.refresh.scanning_preparing": "Taskame mlafukaban fu neo mlafu ima...\nGotova ima...",
"library.refresh.title": "Gengotova al mlafukaban",
"library.scan_library.title": "Taskama mlafuhuomi ima",
"library.scan_library.title": "Taskame mlafuhuomi ima",
"library_info.cleanup": "Parjat",
"library_info.cleanup.backups": "Mverm long mlafuhuomi:",
"library_info.cleanup.dupe_files": "Blisnets mlafu:",
"library_info.cleanup.ignored": "Hestujena shiruzmakaban:",
"library_info.cleanup.legacy_json": "Gammel verso fu mlafuhuomi:",
"library_info.cleanup.unlinked": "Kntsunagajena shiruzmakaban:",
"library_info.stats.colors": "Varge fu festaretol:",
"library_info.stats.entries": "Shiruzmakaban:",
"library_info.stats.fields": "Shiruzmafal:",
"library_info.stats.macros": "Aplinen:",
"library_info.stats.namespaces": "Tumam:",
"library_info.stats.tags": "Festaretol:",
"library_object.name": "Namae",
"library_object.name_required": "Namae (Trengjena)",
@@ -190,15 +223,16 @@
"menu.file.save_library": "Ufne mlafuhuomi",
"menu.help": "&Aputsa",
"menu.help.about": "Shiruplus",
"menu.macros": "&Zetingnen",
"menu.macros": "&Aplinen",
"menu.macros.folders_to_tags": "Mlafukaban festaretol made",
"menu.select": "Sentaku",
"menu.settings": "Sentakuzma...",
"menu.tools": "&Tropos",
"menu.tools.fix_duplicate_files": "Fiks sama &mlafu",
"menu.tools.fix_duplicate_files": "Fiks &blisnets mlafu",
"menu.tools.fix_unlinked_entries": "Fiks &tsunagajenanai shiruzmakaban",
"menu.view": "&Anse",
"menu.window": "Ekrannen",
"namespace.create.description_color": "Varge fu festaretol bruk tumam na vargekaban. Al neo varge mus long tumamklaani na eins.",
"namespace.create.title": "Maha vargetumam",
"namespace.new.button": "Neo vargetumam",
"namespace.new.prompt": "Maha neo vargetumam per nasii varge fu sebja!",
@@ -222,17 +256,17 @@
"settings.show_filenames_in_grid": "Mahase namae fu mlafu ine karta",
"settings.show_recent_libraries": "Mahase moloda mlafuhuomi",
"settings.theme.dark": "Kirai",
"settings.theme.label": "Klea fu zeting:",
"settings.theme.label": "Ekran klea:",
"settings.theme.light": "Kirkas",
"settings.theme.system": "Kompyu",
"settings.title": "Sentakuzma",
"sorting.direction.ascending": "Leste owaris",
"sorting.direction.descending": "Leste eins",
"sorting.direction.ascending": "Plus made",
"sorting.direction.descending": "Minus made",
"splash.opening_library": "Auki mlafuhuomi \"{library_path}\" ima...",
"status.deleted_file_plural": "Kestejenadan {count} mlafu!",
"status.deleted_file_singular": "Kestejenadan 1 mlafu!",
"status.deleted_none": "Nil mlafu kestejenadan.",
"status.deleted_partial_warning": "Mono kestejenadan {count} mlafu! Bitte taskamatsa li du dekinai finna mlafu os ke ima brukjena na andr zeting.",
"status.deleted_partial_warning": "Mono kestejenadan {count} mlafu! Bitte taskamatsa li du dekinai finna mlafu os ke ima brukjena na andr apli.",
"status.deleting_file": "Keste mlafu [{i}/{count}]: \"{path}\" ima...",
"status.library_closing": "Kini mlafuhuomi ima...",
"status.library_save_success": "Mlafuhuomi ufnejenadan au kinijenadan!",
@@ -250,7 +284,7 @@
"tag.all_tags": "Al festaretol",
"tag.choose_color": "Sentaku varge fu festaretol",
"tag.color": "Varge",
"tag.confirm_delete": "Du kestetsa festaretol \"{tag_name}\"?",
"tag.confirm_delete": "Du kestetsa festaretol \"{tag_name}\" we?",
"tag.create": "Maha festaretol",
"tag.create_add": "Maha && Nasii \"{query}\"",
"tag.disambiguation.tooltip": "Bruk afto festaretol grun plusklar",
@@ -272,8 +306,8 @@
"trash.context.singular": "Ugoki mlafu {trash_term} made",
"trash.dialog.disambiguation_warning.plural": "Afto zol keste tuo TagStudio kara <i>AU</i> kompyu fu du kara!",
"trash.dialog.disambiguation_warning.singular": "Afto zol keste sore TagStudio kara <i>AU</i> kompyu fu du kara!",
"trash.dialog.move.confirmation.plural": "Du ugokitsa afto {count} mlafu {trash_term} made?",
"trash.dialog.move.confirmation.singular": "Du ugokitsa afto mlafu {trash_term} made?",
"trash.dialog.move.confirmation.plural": "Du ugokitsa afto {count} mlafu {trash_term} made we?",
"trash.dialog.move.confirmation.singular": "Du ugokitsa afto mlafu {trash_term} made we?",
"trash.dialog.permanent_delete_warning": "<b>VIKTI!</b> Li afto mlafu nai dekijena ugoki {trash_term} made, sore zol <b>kestejena!!</b>",
"trash.dialog.title.plural": "Keste mlafu",
"trash.dialog.title.singular": "Keste mlafu",
Binary file not shown.
+1 -1
View File
@@ -29,7 +29,7 @@ def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver
# Set the widget
tags = library.tags
panel.set_tag_widget(tags[0], 0)
tag_widget: TagWidget = panel.scroll_layout.itemAt(0).widget()
tag_widget: TagWidget = panel.scroll_layout.itemAt(0).widget() # pyright: ignore[reportAssignmentType]
should_replace_actions = {
tag_widget: ["on_edit()", "on_remove()"],
+1
View File
@@ -30,6 +30,7 @@ EMPTY_LIBRARIES = "empty_libraries"
# str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_102")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_103")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_200")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_201")),
],
)
def test_library_migrations(path: str):