Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ runs:
if: inputs.skip-setup != 'true'
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.14'

- name: Setup uv install
if: inputs.skip-setup != 'true'
Expand Down
6 changes: 3 additions & 3 deletions docs/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ If you wish to develop for TagStudio, you'll need to create a development enviro

## Installing Python

Python [3.12](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
Python [3.14](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.14.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.

<!-- prettier-ignore -->
!!! info "Python Aliases"
Expand All @@ -35,8 +35,8 @@ python --version
If you choose to install Python using pyenv, please refer to the following instructions:

1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system.
2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation).
3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
2. Install the appropriate Python version with pyenv by running `pyenv install 3.14` (This will **not** mess with your existing Python installation).
3. Navigate to the repository root folder in your terminal and run `pyenv local 3.14`. You could alternatively use `pyenv shell 3.14` or `pyenv global 3.14` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-

### :fontawesome-brands-python: Installing with PIP

TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.12 and PIP installed if you choose to install using this method.
TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.14 and PIP installed if you choose to install using this method.

The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).

Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ description = "A User-Focused Photo & File Management System."
version = "9.6.4"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.14"
requires-python = ">=3.14,<3.15"
dependencies = [
"audioop-lts; python_version >= '3.13'",
"audioop-lts~=0.2.2",
"chardet~=5.2",
"ffmpeg-python~=0.2",
"humanfriendly==10.*",
"mutagen~=1.47",
"numpy~=2.2",
"opencv_python~=4.11",
"Pillow>=10.2,<12",
"pillow-heif~=0.22",
"pillow-heif~=1.5.0",
"pillow-jxl-plugin~=1.3",
"py7zr~=1.1.3",
"pydantic~=2.10",
"pydub~=0.25",
"PySide6==6.8.0.*",
"PySide6==6.11.2",
"rarfile==4.2",
"rawpy~=0.27",
"Send2Trash>=1.8,<3",
Expand Down
28 changes: 13 additions & 15 deletions src/tagstudio/core/library/alchemy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class TagColorEnum(enum.IntEnum):
OLIVE = 37

@staticmethod
def get_color_from_str(color_name: str) -> "TagColorEnum":
def get_color_from_str(color_name: str) -> TagColorEnum:
for color in TagColorEnum:
if color.name == color_name.upper().replace(" ", "_"):
return color
Expand Down Expand Up @@ -99,17 +99,15 @@ def ast(self) -> AST | None:
return Parser(self.query).parse()

@classmethod
def show_all(cls) -> "BrowsingState":
def show_all(cls) -> BrowsingState:
return BrowsingState()

@classmethod
def from_search_query(cls, search_query: str) -> "BrowsingState":
def from_search_query(cls, search_query: str) -> BrowsingState:
return cls(query=search_query)

@classmethod
def from_tag_id(
cls, tag_id: int | str, state: "BrowsingState | None" = None
) -> "BrowsingState":
def from_tag_id(cls, tag_id: int | str, state: BrowsingState | None = None) -> BrowsingState:
"""Create and return a BrowsingState object given a tag ID.

Args:
Expand All @@ -124,35 +122,35 @@ def from_tag_id(
return cls(query=f"tag_id:{str(tag_id)}")

@classmethod
def from_path(cls, path: Path | str) -> "BrowsingState":
def from_path(cls, path: Path | str) -> BrowsingState:
return cls(query=f'path:"{str(path).strip()}"')

@classmethod
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
def from_mediatype(cls, mediatype: str) -> BrowsingState:
return cls(query=f"mediatype:{mediatype}")

@classmethod
def from_filetype(cls, filetype: str) -> "BrowsingState":
def from_filetype(cls, filetype: str) -> BrowsingState:
return cls(query=f"filetype:{filetype}")

@classmethod
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
def from_tag_name(cls, tag_name: str) -> BrowsingState:
return cls(query=f'tag:"{tag_name}"')

def with_page_index(self, index: int) -> "BrowsingState":
def with_page_index(self, index: int) -> BrowsingState:
return replace(self, page_index=index)

def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
def with_sorting_mode(self, mode: SortingModeEnum) -> BrowsingState:
seed = self.random_seed
if mode == SortingModeEnum.RANDOM:
seed = random.random()
return replace(self, sorting_mode=mode, random_seed=seed)

def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
def with_sorting_direction(self, ascending: bool) -> BrowsingState:
return replace(self, ascending=ascending)

def with_search_query(self, search_query: str) -> "BrowsingState":
def with_search_query(self, search_query: str) -> BrowsingState:
return replace(self, query=search_query)

def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
def with_show_hidden_entries(self, show_hidden_entries: bool) -> BrowsingState:
return replace(self, show_hidden_entries=show_hidden_entries)
18 changes: 9 additions & 9 deletions src/tagstudio/core/library/alchemy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class TagAlias(Base):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"))
tag: Mapped["Tag"] = relationship(back_populates="aliases")
tag: Mapped[Tag] = relationship(back_populates="aliases")

def __init__(self, name: str, tag_id: int | None = None):
self.name = name
Expand Down Expand Up @@ -97,14 +97,14 @@ class Tag(Base):
is_hidden: Mapped[bool]
icon: Mapped[str | None]
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
parent_tags: Mapped[set["Tag"]] = relationship(
parent_tags: Mapped[set[Tag]] = relationship(
secondary=TagParent.__tablename__,
primaryjoin="Tag.id == TagParent.child_id",
secondaryjoin="Tag.id == TagParent.parent_id",
back_populates="parent_tags",
)
disambiguation_id: Mapped[int | None]
category_exclusions: Mapped[set["Tag"]] = relationship(
category_exclusions: Mapped[set[Tag]] = relationship(
secondary=CategoryExclusion.__tablename__,
primaryjoin="Tag.id == CategoryExclusion.tag_id",
secondaryjoin="Tag.id == CategoryExclusion.category_id",
Expand Down Expand Up @@ -140,14 +140,14 @@ def __init__(
id: int | None = None,
shorthand: str | None = None,
aliases: set[TagAlias] | None = None,
parent_tags: set["Tag"] | None = None,
parent_tags: set[Tag] | None = None,
icon: str | None = None,
color_namespace: str | None = None,
color_slug: str | None = None,
disambiguation_id: int | None = None,
is_category: bool = False,
is_hidden: bool = False,
category_exclusions: set["Tag"] | None = None,
category_exclusions: set[Tag] | None = None,
):
self.name = name
self.aliases = aliases or set()
Expand Down Expand Up @@ -181,16 +181,16 @@ def __eq__(self, value: object) -> bool:
return False
return self.id == value.id

def __lt__(self, other: "Tag") -> bool:
def __lt__(self, other: Tag) -> bool:
return self.name < other.name

def __le__(self, other: "Tag") -> bool:
def __le__(self, other: Tag) -> bool:
return self.name <= other.name

def __gt__(self, other: "Tag") -> bool:
def __gt__(self, other: Tag) -> bool:
return self.name > other.name

def __ge__(self, other: "Tag") -> bool:
def __ge__(self, other: Tag) -> bool:
return self.name >= other.name


Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/core/library/json/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ def refresh_dir(self) -> Generator:
self.files_not_in_library,
key=lambda t: -(self.library_dir / t).stat().st_ctime,
)
except (FileExistsError, FileNotFoundError):
except FileExistsError, FileNotFoundError:
print(
"[LIBRARY] [ERROR] Couldn't sort files, some were moved during the scanning/sorting process."
)
Expand Down
8 changes: 4 additions & 4 deletions src/tagstudio/core/query_lang/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ConstraintType(Enum):
Special = 5

@staticmethod
def from_string(text: str) -> "ConstraintType | None":
def from_string(text: str) -> ConstraintType | None:
return {
"tag": ConstraintType.Tag,
"tag_id": ConstraintType.TagID,
Expand All @@ -28,7 +28,7 @@ def from_string(text: str) -> "ConstraintType | None":


class AST:
parent: "AST | None" = None
parent: AST | None = None

@override
def __str__(self):
Expand Down Expand Up @@ -65,9 +65,9 @@ def __init__(self, elements: list[AST]) -> None:
class Constraint(AST):
type: ConstraintType
value: str
properties: list["Property"]
properties: list[Property]

def __init__(self, type: ConstraintType, value: str, properties: list["Property"]) -> None:
def __init__(self, type: ConstraintType, value: str, properties: list[Property]) -> None:
super().__init__()
for prop in properties:
prop.parent = self
Expand Down
4 changes: 2 additions & 2 deletions src/tagstudio/core/query_lang/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ def __init__(
self.end = end

@staticmethod
def from_type(type: TokenType, pos: int) -> "Token":
def from_type(type: TokenType, pos: int) -> Token:
return Token(type, None, pos, pos)

@staticmethod
def EOF(pos: int) -> "Token": # noqa: N802
def EOF(pos: int) -> Token: # noqa: N802
return Token.from_type(TokenType.EOF, pos)

@override
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/core/utils/str_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ def format_duration(duration: int | float) -> str:
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
return f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes}:{seconds:02}"
except (OverflowError, ValueError):
except OverflowError, ValueError:
return "-:--"
2 changes: 1 addition & 1 deletion src/tagstudio/i18n/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def change_language(self, lang: str):
def __format(self, text: str, **kwargs: ...) -> str:
try:
return text.format(**kwargs)
except (KeyError, ValueError):
except KeyError, ValueError:
logger.error(
"[Translations] Error while formatting translation.",
text=text,
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/previews/renderers/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def namelist(self) -> list[str]:
def read(self, name: str) -> bytes:
return unwrap(self.tar.extractfile(name)).read()

def __enter__(self) -> "TarFile":
def __enter__(self) -> TarFile:
self.tar = tarfile.open(name=self.filepath, mode=self.mode).__enter__()
return self

Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/previews/renderers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def audio_album_thumb(filepath: Path, ext: str) -> Image.Image | None:
image = artwork
except (
FileNotFoundError,
id3.ID3NoHeaderError, # pyright: ignore[reportPrivateImportUsage]
id3.ID3NoHeaderError,
mp4.MP4MetadataError,
mp4.MP4StreamInfoError,
MutagenError,
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/qt/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class AppSettings(BaseModel):
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)

@staticmethod
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "AppSettings":
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> AppSettings:
if path.exists():
with open(path) as file:
filecontents = file.read()
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/qt/controllers/autofill_line_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ def show_action_menu(self, pos: QPoint) -> None:
# Filter out icon action(s)
if action.text():
menu.addAction(action)
menu.exec(self.mapToGlobal(pos)) # pyright: ignore[reportArgumentType]
menu.exec(self.mapToGlobal(pos))
4 changes: 3 additions & 1 deletion src/tagstudio/qt/controllers/field_suggest_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.app_settings import AppSettings
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
Expand Down Expand Up @@ -155,6 +156,7 @@ def _get_item_widget(self, index: int, library: Library | None) -> UnderlinedWid
widget.setHidden(True)
self.layout().content_layout.addWidget(widget)

widget_: QWidget = self.layout().content_layout.itemAt(index).widget()
item = unwrap(self.layout().content_layout.itemAt(index))
widget_: QWidget = unwrap(item.widget())
assert isinstance(widget_, UnderlinedWidget)
return widget_
4 changes: 3 additions & 1 deletion src/tagstudio/qt/controllers/field_template_search_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
from tagstudio.qt.controllers.field_template_widget import FieldTemplateWidget
Expand Down Expand Up @@ -173,6 +174,7 @@ def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateW
pad_field_template_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_field_template_widget)

field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
item = unwrap(self.layout().scroll_layout.itemAt(index))
field_template_widget: QWidget = unwrap(item.widget())
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
2 changes: 1 addition & 1 deletion src/tagstudio/qt/controllers/fix_ignored_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

# TODO: Use newer MVC style guidelines
class FixIgnoredEntriesModal(FixIgnoredEntriesModalView):
def __init__(self, library: "Library", driver: "QtDriver"):
def __init__(self, library: Library, driver: QtDriver):
super().__init__(library, driver)
self.tracker = IgnoredRegistry(self.lib)

Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/qt/controllers/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class _ItemMode(IntEnum):


class Inspector(QWidget):
def __init__(self, driver: "QtDriver") -> None:
def __init__(self, driver: QtDriver) -> None:
super().__init__()
self._driver = driver
self._lib = self._driver.lib
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/qt/controllers/library_info_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

# TODO: Use newer MVC style guidelines
class LibraryInfoWindow(LibraryInfoWindowView):
def __init__(self, library: "Library", driver: "QtDriver"):
def __init__(self, library: Library, driver: QtDriver):
super().__init__(library, driver)

# Statistics Buttons
Expand Down
Loading
Loading