Compare commits

...

4 Commits

Author SHA1 Message Date
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
29 changed files with 173 additions and 130 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.
+6 -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,6 +74,7 @@ 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
@@ -106,11 +82,11 @@ reportImportCycles = false
reportMissingTypeArgument = false
reportMissingTypeStubs = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownLambdaType = false
reportUnknownMemberType = false
reportUnusedCallResult = false
reportUnannotatedClassAttribute = false
reportUninitializedInstanceVariable = false
[tool.ruff]
@@ -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)
@@ -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)
+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)
+13 -13
View File
@@ -299,7 +299,7 @@ class ThumbRenderer(QObject):
im: Image.Image = Image.new(
mode="L",
size=tuple([d * smooth_factor for d in size]), # type: ignore # pyright: ignore[reportArgumentType]
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="black",
)
draw = ImageDraw.Draw(im)
@@ -330,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 # pyright: ignore[reportArgumentType]
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#00000000",
)
draw = ImageDraw.Draw(im_hl)
@@ -349,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 # pyright: ignore[reportArgumentType]
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#00000000",
)
draw = ImageDraw.Draw(im_sh)
@@ -394,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 # pyright: ignore[reportArgumentType]
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#FF000000",
)
@@ -402,13 +402,13 @@ class ThumbRenderer(QObject):
bg: Image.Image
bg = Image.new(
"RGB",
size=tuple([d * smooth_factor for d in size]), # type: ignore # pyright: ignore[reportArgumentType]
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 # pyright: ignore[reportArgumentType]
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)
@@ -417,7 +417,7 @@ class ThumbRenderer(QObject):
bg,
(0, 0),
mask=self._get_mask(
tuple([d * smooth_factor for d in size]), # type: ignore # pyright: ignore[reportArgumentType]
tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
(pixel_ratio * smooth_factor),
),
)
@@ -501,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 # pyright: ignore[reportArgumentType]
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 # pyright: ignore[reportArgumentType]
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 # pyright: ignore[reportArgumentType]
size=tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
color="#000000",
)
# Apply color overlay
@@ -527,7 +527,7 @@ class ThumbRenderer(QObject):
bg,
(0, 0),
mask=self._get_mask(
tuple([d * smooth_factor for d in size]), # type: ignore # pyright: ignore[reportArgumentType]
tuple([d * smooth_factor for d in size]), # pyright: ignore[reportArgumentType]
(pixel_ratio * smooth_factor),
),
)
@@ -1216,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(device=buffer, format="PNG") # type: ignore # pyright: ignore[reportArgumentType]
q_image.save(device=buffer, format="PNG") # pyright: ignore[reportArgumentType]
# Load the image from the buffer
im = Image.new("RGB", (size, size), color="#1e1e1e")
@@ -1338,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()
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from pydub
# type: ignore
import array
+2 -2
View File
@@ -350,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")
@@ -1100,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)
+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)
Binary file not shown.
+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):